diff --git a/CODEOWNERS b/CODEOWNERS index f85993bd87..dbd3d2c592 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -294,6 +294,7 @@ esphome/components/microphone/* @jesserockz @kahrendt esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov +esphome/components/mipi_dsi/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey esphome/components/mixer/speaker/* @kahrendt diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 387df10c32..b9299bb8d7 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -230,7 +230,7 @@ class DriverChip: ): name = name.upper() self.name = name - self.initsequence = initsequence + self.initsequence = initsequence or defaults.get("init_sequence") self.defaults = defaults DriverChip.models[name] = self @@ -347,7 +347,7 @@ class DriverChip: Pixel format, color order, and orientation will be set. Returns a tuple of the init sequence and the computed MADCTL value. """ - sequence = list(self.initsequence) + sequence = list(self.initsequence or ()) custom_sequence = config.get(CONF_INIT_SEQUENCE, []) sequence.extend(custom_sequence) # Ensure each command is a tuple @@ -356,6 +356,8 @@ class DriverChip: # Set pixel format if not already in the custom sequence pixel_mode = config[CONF_PIXEL_MODE] if not isinstance(pixel_mode, int): + if not pixel_mode.endswith("bit"): + pixel_mode = f"{pixel_mode}bit" pixel_mode = PIXEL_MODES[pixel_mode] sequence.append((PIXFMT, pixel_mode)) diff --git a/esphome/components/mipi_dsi/__init__.py b/esphome/components/mipi_dsi/__init__.py new file mode 100644 index 0000000000..84dd66e2f4 --- /dev/null +++ b/esphome/components/mipi_dsi/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] + +mipi_dsi_ns = cg.esphome_ns.namespace("mipi_dsi") diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py new file mode 100644 index 0000000000..4ed70a04c2 --- /dev/null +++ b/esphome/components/mipi_dsi/display.py @@ -0,0 +1,232 @@ +import importlib +import logging +import pkgutil + +from esphome import pins +import esphome.codegen as cg +from esphome.components import display +from esphome.components.const import ( + BYTE_ORDER_BIG, + BYTE_ORDER_LITTLE, + CONF_BYTE_ORDER, + CONF_DRAW_ROUNDING, +) +from esphome.components.display import CONF_SHOW_TEST_CARD +from esphome.components.esp32 import const, only_on_variant +from esphome.components.mipi import ( + COLOR_ORDERS, + CONF_COLOR_DEPTH, + CONF_HSYNC_BACK_PORCH, + CONF_HSYNC_FRONT_PORCH, + CONF_HSYNC_PULSE_WIDTH, + CONF_PCLK_FREQUENCY, + CONF_PIXEL_MODE, + CONF_USE_AXIS_FLIPS, + CONF_VSYNC_BACK_PORCH, + CONF_VSYNC_FRONT_PORCH, + CONF_VSYNC_PULSE_WIDTH, + MODE_BGR, + PIXEL_MODE_16BIT, + PIXEL_MODE_24BIT, + DriverChip, + dimension_schema, + get_color_depth, + map_sequence, + power_of_two, + requires_buffer, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_COLOR_ORDER, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_ID, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_WIDTH, +) +from esphome.final_validate import full_config + +from . import mipi_dsi_ns, models + +DEPENDENCIES = ["esp32"] +DOMAIN = "mipi_dsi" + +LOGGER = logging.getLogger(DOMAIN) + +MIPI_DSI = mipi_dsi_ns.class_("MIPI_DSI", display.Display, cg.Component) +ColorOrder = display.display_ns.enum("ColorMode") +ColorBitness = display.display_ns.enum("ColorBitness") + +CONF_LANE_BIT_RATE = "lane_bit_rate" +CONF_LANES = "lanes" + +DriverChip("CUSTOM") + +# Import all models dynamically from the models package + +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) + +MODELS = DriverChip.get_models() + +COLOR_DEPTHS = { + 16: ColorBitness.COLOR_BITNESS_565, + 24: ColorBitness.COLOR_BITNESS_888, +} + + +def model_schema(config): + model = MODELS[config[CONF_MODEL].upper()] + transform = cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ) + if model.get_default(CONF_SWAP_XY) != cv.UNDEFINED: + transform = transform.extend( + { + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by this model" + ) + } + ) + else: + transform = transform.extend( + { + cv.Required(CONF_SWAP_XY): cv.boolean, + } + ) + # CUSTOM model will need to provide a custom init sequence + iseqconf = ( + cv.Required(CONF_INIT_SEQUENCE) + if model.initsequence is None + else cv.Optional(CONF_INIT_SEQUENCE) + ) + swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) + + # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden + cv_dimensions = ( + cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required + ) + pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24") + schema = display.FULL_DISPLAY_SCHEMA.extend( + { + model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(MIPI_DSI), + cv_dimensions(CONF_DIMENSIONS): dimension_schema( + model.get_default(CONF_DRAW_ROUNDING, 1) + ), + model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True), + model.option(CONF_DRAW_ROUNDING, 2): power_of_two, + model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( + *pixel_modes, lower=True + ), + model.option(CONF_TRANSFORM, cv.UNDEFINED): transform, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + model.option(CONF_INVERT_COLORS, False): cv.boolean, + model.option(CONF_COLOR_DEPTH, "16"): cv.one_of( + *[str(d) for d in COLOR_DEPTHS], + *[f"{d}bit" for d in COLOR_DEPTHS], + lower=True, + ), + model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, + model.option(CONF_PCLK_FREQUENCY, "40MHz"): cv.All( + cv.frequency, cv.Range(min=4e6, max=100e6) + ), + model.option(CONF_LANES, 2): cv.int_range(1, 2), + model.option(CONF_LANE_BIT_RATE, None): cv.All( + cv.bps, cv.Range(min=100e6, max=3200e6) + ), + iseqconf: cv.ensure_list(map_sequence), + model.option(CONF_BYTE_ORDER, BYTE_ORDER_LITTLE): cv.one_of( + BYTE_ORDER_LITTLE, BYTE_ORDER_BIG, lower=True + ), + model.option(CONF_HSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_HSYNC_BACK_PORCH): cv.int_, + model.option(CONF_HSYNC_FRONT_PORCH): cv.int_, + model.option(CONF_VSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_VSYNC_BACK_PORCH): cv.int_, + model.option(CONF_VSYNC_FRONT_PORCH): cv.int_, + } + ) + return cv.All( + schema, + only_on_variant(supported=[const.VARIANT_ESP32P4]), + cv.only_with_esp_idf, + ) + + +def _config_schema(config): + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=cv.ALLOW_EXTRA, + )(config) + return model_schema(config)(config) + + +def _final_validate(config): + global_config = full_config.get() + + from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + + if not requires_buffer(config) and LVGL_DOMAIN not in global_config: + # If no drawing methods are configured, and LVGL is not enabled, show a test card + config[CONF_SHOW_TEST_CARD] = True + return config + + +CONFIG_SCHEMA = _config_schema +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + model = MODELS[config[CONF_MODEL].upper()] + color_depth = COLOR_DEPTHS[get_color_depth(config)] + pixel_mode = int(config[CONF_PIXEL_MODE].removesuffix("bit")) + width, height, _offset_width, _offset_height = model.get_dimensions(config) + var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode) + + sequence, madctl = model.get_sequence(config) + cg.add(var.set_model(config[CONF_MODEL])) + cg.add(var.set_init_sequence(sequence)) + cg.add(var.set_madctl(madctl)) + cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) + cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH])) + cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH])) + cg.add(var.set_hsync_front_porch(config[CONF_HSYNC_FRONT_PORCH])) + cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) + cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) + cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) + cg.add(var.set_pclk_frequency(int(config[CONF_PCLK_FREQUENCY] / 1e6))) + cg.add(var.set_lanes(int(config[CONF_LANES]))) + cg.add(var.set_lane_bit_rate(int(config[CONF_LANE_BIT_RATE] / 1e6))) + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + if enable_pin := config.get(CONF_ENABLE_PIN): + enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] + cg.add(var.set_enable_pins(enable)) + + if model.rotation_as_transform(config): + config[CONF_ROTATION] = 0 + await display.register_display(var, config) + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp new file mode 100644 index 0000000000..fbe251de41 --- /dev/null +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -0,0 +1,379 @@ +#ifdef USE_ESP32_VARIANT_ESP32P4 +#include +#include "mipi_dsi.h" + +namespace esphome { +namespace mipi_dsi { + +static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) { + auto *sem = static_cast(user_ctx); + BaseType_t need_yield = pdFALSE; + xSemaphoreGiveFromISR(sem, &need_yield); + return (need_yield == pdTRUE); +} +void MIPI_DSI::setup() { + ESP_LOGCONFIG(TAG, "Running Setup"); + + if (!this->enable_pins_.empty()) { + for (auto *pin : this->enable_pins_) { + pin->setup(); + pin->digital_write(true); + } + delay(10); + } + + esp_lcd_dsi_bus_config_t bus_config = { + .bus_id = 0, // index from 0, specify the DSI host to use + .num_data_lanes = + this->lanes_, // Number of data lanes to use, can't set a value that exceeds the chip's capability + .phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, // Clock source for the DPHY + .lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps + }; + auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_); + if (err != ESP_OK) { + this->smark_failed("lcd_new_dsi_bus failed", err); + return; + } + esp_lcd_dbi_io_config_t dbi_config = { + .virtual_channel = 0, + .lcd_cmd_bits = 8, // according to the LCD spec + .lcd_param_bits = 8, // according to the LCD spec + }; + err = esp_lcd_new_panel_io_dbi(this->bus_handle_, &dbi_config, &this->io_handle_); + if (err != ESP_OK) { + this->smark_failed("new_panel_io_dbi failed", err); + return; + } + auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + if (this->color_depth_ == display::COLOR_BITNESS_888) { + pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB888; + } + esp_lcd_dpi_panel_config_t dpi_config = {.virtual_channel = 0, + .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, + .dpi_clock_freq_mhz = this->pclk_frequency_, + .pixel_format = pixel_format, + .num_fbs = 1, // number of frame buffers to allocate + .video_timing = + { + .h_size = this->width_, + .v_size = this->height_, + .hsync_pulse_width = this->hsync_pulse_width_, + .hsync_back_porch = this->hsync_back_porch_, + .hsync_front_porch = this->hsync_front_porch_, + .vsync_pulse_width = this->vsync_pulse_width_, + .vsync_back_porch = this->vsync_back_porch_, + .vsync_front_porch = this->vsync_front_porch_, + }, + .flags = { + .use_dma2d = true, + }}; + err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_); + if (err != ESP_OK) { + this->smark_failed("esp_lcd_new_panel_dpi failed", err); + return; + } + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + } else { + esp_lcd_panel_io_tx_param(this->io_handle_, SW_RESET_CMD, nullptr, 0); + } + // need to know when the display is ready for SLPOUT command - will be 120ms after reset + auto when = millis() + 120; + err = esp_lcd_panel_init(this->handle_); + if (err != ESP_OK) { + this->smark_failed("esp_lcd_init failed", err); + return; + } + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + this->mark_failed("Malformed init sequence"); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + ESP_LOGD(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + this->mark_failed("Malformed init sequence"); + return; + } + if (cmd == SLEEP_OUT) { + // are we ready, boots? + int duration = when - millis(); + if (duration > 0) { + delay(duration); + } + } + const auto *ptr = vec.data() + index; + ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, num_args, + format_hex_pretty(ptr, num_args, '.', false).c_str()); + err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args); + if (err != ESP_OK) { + this->smark_failed("lcd_panel_io_tx_param failed", err); + return; + } + index += num_args; + if (cmd == SLEEP_OUT) + delay(10); + } + } + this->io_lock_ = xSemaphoreCreateBinary(); + esp_lcd_dpi_panel_event_callbacks_t cbs = { + .on_color_trans_done = notify_refresh_ready, + }; + + err = (esp_lcd_dpi_panel_register_event_callbacks(this->handle_, &cbs, this->io_lock_)); + if (err != ESP_OK) { + this->smark_failed("Failed to register callbacks", err); + return; + } + + ESP_LOGCONFIG(TAG, "MIPI DSI setup complete"); +} + +void MIPI_DSI::update() { + if (this->auto_clear_enabled_) { + this->clear(); + } + if (this->show_test_card_) { + this->test_card(); + } else if (this->page_ != nullptr) { + this->page_->get_writer()(*this); + } else if (this->writer_.has_value()) { + (*this->writer_)(*this); + } else { + this->stop_poller(); + } + if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) + return; + ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_); + int w = this->x_high_ - this->x_low_ + 1; + int h = this->y_high_ - this->y_low_ + 1; + this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_, + this->width_ - w - this->x_low_); + // invalidate watermarks + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; +} + +void MIPI_DSI::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { + if (w <= 0 || h <= 0) + return; + // if color mapping is required, pass the buck. + // note that endianness is not considered here - it is assumed to match! + if (bitness != this->color_depth_) { + display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, + x_pad); + } + this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); +} + +void MIPI_DSI::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad) { + esp_err_t err = ESP_OK; + auto bytes_per_pixel = 3 - this->color_depth_; + auto stride = (x_offset + w + x_pad) * bytes_per_pixel; + ptr += y_offset * stride + x_offset * bytes_per_pixel; // skip to the first pixel + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + if (x_offset == 0 && x_pad == 0) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y_start, x_start + w, y_start + h, ptr); + xSemaphoreTake(this->io_lock_, portMAX_DELAY); + + } else { + // draw line by line + for (int y = 0; y != h; y++) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y + y_start, x_start + w, y + y_start + 1, ptr); + if (err != ESP_OK) + break; + ptr += stride; // next line + xSemaphoreTake(this->io_lock_, portMAX_DELAY); + } + } + if (err != ESP_OK) + ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); +} + +bool MIPI_DSI::check_buffer_() { + if (this->is_failed()) + return false; + if (this->buffer_ != nullptr) + return true; + // this is dependent on the enum values. + auto bytes_per_pixel = 3 - this->color_depth_; + RAMAllocator allocator; + this->buffer_ = allocator.allocate(this->height_ * this->width_ * bytes_per_pixel); + if (this->buffer_ == nullptr) { + this->mark_failed("Could not allocate buffer for display!"); + return false; + } + return true; +} + +void MIPI_DSI::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; + + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + std::swap(x, y); + x = this->width_ - x - 1; + break; + case display::DISPLAY_ROTATION_180_DEGREES: + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + case display::DISPLAY_ROTATION_270_DEGREES: + std::swap(x, y); + y = this->height_ - y - 1; + break; + } + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + auto pixel = convert_big_endian(display::ColorUtil::color_to_565(color)); + if (!this->check_buffer_()) + return; + size_t pos = (y * this->width_) + x; + switch (this->color_depth_) { + case display::COLOR_BITNESS_565: { + auto *ptr_16 = reinterpret_cast(this->buffer_); + uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); + uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); + uint16_t new_color = lo_byte | (hi_byte << 8); // little endian + if (ptr_16[pos] == new_color) + return; + ptr_16[pos] = new_color; + break; + } + case display::COLOR_BITNESS_888: + if (this->color_mode_ == display::COLOR_ORDER_BGR) { + this->buffer_[pos * 3] = color.b; + this->buffer_[pos * 3 + 1] = color.g; + this->buffer_[pos * 3 + 2] = color.r; + } else { + this->buffer_[pos * 3] = color.r; + this->buffer_[pos * 3 + 1] = color.g; + this->buffer_[pos * 3 + 2] = color.b; + } + break; + case display::COLOR_BITNESS_332: + break; + } + // low and high watermark may speed up drawing from buffer + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; +} +void MIPI_DSI::fill(Color color) { + if (!this->check_buffer_()) + return; + switch (this->color_depth_) { + case display::COLOR_BITNESS_565: { + auto *ptr_16 = reinterpret_cast(this->buffer_); + uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); + uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); + uint16_t new_color = lo_byte | (hi_byte << 8); // little endian + std::fill_n(ptr_16, this->width_ * this->height_, new_color); + break; + } + + case display::COLOR_BITNESS_888: + if (this->color_mode_ == display::COLOR_ORDER_BGR) { + for (size_t i = 0; i != this->width_ * this->height_; i++) { + this->buffer_[i * 3 + 0] = color.b; + this->buffer_[i * 3 + 1] = color.g; + this->buffer_[i * 3 + 2] = color.r; + } + } else { + for (size_t i = 0; i != this->width_ * this->height_; i++) { + this->buffer_[i * 3 + 0] = color.r; + this->buffer_[i * 3 + 1] = color.g; + this->buffer_[i * 3 + 2] = color.b; + } + } + + default: + break; + } +} + +int MIPI_DSI::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int MIPI_DSI::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + +static const uint8_t PIXEL_MODES[] = {0, 16, 18, 24}; + +void MIPI_DSI::dump_config() { + ESP_LOGCONFIG(TAG, + "MIPI_DSI RGB LCD" + "\n Model: %s" + "\n Width: %u" + "\n Height: %u" + "\n Mirror X: %s" + "\n Mirror Y: %s" + "\n Swap X/Y: %s" + "\n Rotation: %d degrees" + "\n DSI Lanes: %u" + "\n Lane Bit Rate: %uMbps" + "\n HSync Pulse Width: %u" + "\n HSync Back Porch: %u" + "\n HSync Front Porch: %u" + "\n VSync Pulse Width: %u" + "\n VSync Back Porch: %u" + "\n VSync Front Porch: %u" + "\n Buffer Color Depth: %d bit" + "\n Display Pixel Mode: %d bit" + "\n Color Order: %s" + "\n Invert Colors: %s" + "\n Pixel Clock: %dMHz", + this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), + YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_, + this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_, + this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, this->vsync_front_porch_, + (3 - this->color_depth_) * 8, this->pixel_mode_, this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", + YESNO(this->invert_colors_), this->pclk_frequency_); + LOG_PIN(" Reset Pin ", this->reset_pin_); +} +} // namespace mipi_dsi +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32P4 diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h new file mode 100644 index 0000000000..ce8a2a2236 --- /dev/null +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -0,0 +1,123 @@ +// +// Created by Clyde Stubbs on 29/10/2023. +// +#pragma once + +// only applicable on ESP32-P4 +#ifdef USE_ESP32_VARIANT_ESP32P4 +#include "esphome/core/component.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/gpio.h" + +#include "esphome/components/display/display.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_io.h" + +#include "esp_lcd_mipi_dsi.h" + +namespace esphome { +namespace mipi_dsi { + +constexpr static const char *const TAG = "display.mipi_dsi"; +const uint8_t SW_RESET_CMD = 0x01; +const uint8_t SLEEP_OUT = 0x11; +const uint8_t SDIR_CMD = 0xC7; +const uint8_t MADCTL_CMD = 0x36; +const uint8_t INVERT_OFF = 0x20; +const uint8_t INVERT_ON = 0x21; +const uint8_t DISPLAY_ON = 0x29; +const uint8_t CMD2_BKSEL = 0xFF; +const uint8_t DELAY_FLAG = 0xFF; +const uint8_t MADCTL_BGR = 0x08; +const uint8_t MADCTL_MX = 0x40; +const uint8_t MADCTL_MY = 0x80; +const uint8_t MADCTL_MV = 0x20; // row/column swap +const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally +const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically + +class MIPI_DSI : public display::Display { + public: + MIPI_DSI(size_t width, size_t height, display::ColorBitness color_depth, uint8_t pixel_mode) + : width_(width), height_(height), color_depth_(color_depth), pixel_mode_(pixel_mode) {} + display::ColorOrder get_color_mode() { return this->color_mode_; } + void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } + void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } + void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } + void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; } + void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; } + void set_vsync_pulse_width(uint16_t vsync_pulse_width) { this->vsync_pulse_width_ = vsync_pulse_width; } + void set_vsync_back_porch(uint16_t vsync_back_porch) { this->vsync_back_porch_ = vsync_back_porch; } + void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } + void set_init_sequence(const std::vector &init_sequence) { this->init_sequence_ = init_sequence; } + void set_model(const char *model) { this->model_ = model; } + void set_lane_bit_rate(uint16_t lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; } + void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } + void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } + + void smark_failed(const char *message, esp_err_t err) { + auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err)); + this->mark_failed(str.c_str()); + } + + void update() override; + + void setup() override; + + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; + + void draw_pixel_at(int x, int y, Color color) override; + void fill(Color color) override; + int get_width() override; + int get_height() override; + + void dump_config() override; + + protected: + void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad); + bool check_buffer_(); + GPIOPin *reset_pin_{nullptr}; + std::vector enable_pins_{}; + size_t width_{}; + size_t height_{}; + uint8_t madctl_{}; + uint16_t hsync_pulse_width_ = 10; + uint16_t hsync_back_porch_ = 10; + uint16_t hsync_front_porch_ = 20; + uint16_t vsync_pulse_width_ = 10; + uint16_t vsync_back_porch_ = 10; + uint16_t vsync_front_porch_ = 10; + const char *model_{"Unknown"}; + std::vector init_sequence_{}; + uint16_t pclk_frequency_ = 16; // in MHz + uint16_t lane_bit_rate_{1500}; // in Mbps + uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes + + bool invert_colors_{}; + display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; + display::ColorBitness color_depth_; + uint8_t pixel_mode_{}; + + esp_lcd_panel_handle_t handle_{}; + esp_lcd_dsi_bus_handle_t bus_handle_{}; + esp_lcd_panel_io_handle_t io_handle_{}; + SemaphoreHandle_t io_lock_{}; + uint8_t *buffer_{nullptr}; + uint16_t x_low_{1}; + uint16_t y_low_{1}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; +}; + +} // namespace mipi_dsi +} // namespace esphome +#endif diff --git a/esphome/components/mipi_dsi/models/__init__.py b/esphome/components/mipi_dsi/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py new file mode 100644 index 0000000000..fd3fbf6160 --- /dev/null +++ b/esphome/components/mipi_dsi/models/guition.py @@ -0,0 +1,38 @@ +from esphome.components.mipi import DriverChip +import esphome.config_validation as cv + +# fmt: off +DriverChip( + "JC1060P470", + width=1024, + height=600, + hsync_back_porch=160, + hsync_pulse_width=40, + hsync_front_porch=160, + vsync_back_porch=23, + vsync_pulse_width=10, + vsync_front_porch=12, + pclk_frequency="54MHz", + lane_bit_rate="750Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + reset_pin=27, + initsequence=[ + (0x30, 0x00), (0xF7, 0x49, 0x61, 0x02, 0x00), (0x30, 0x01), (0x04, 0x0C), (0x05, 0x00), (0x06, 0x00), + (0x0B, 0x11), (0x17, 0x00), (0x20, 0x04), (0x1F, 0x05), (0x23, 0x00), (0x25, 0x19), (0x28, 0x18), (0x29, 0x04), (0x2A, 0x01), + (0x2B, 0x04), (0x2C, 0x01), (0x30, 0x02), (0x01, 0x22), (0x03, 0x12), (0x04, 0x00), (0x05, 0x64), (0x0A, 0x08), + (0x0B, 0x0A, 0x1A, 0x0B, 0x0D, 0x0D, 0x11, 0x10, 0x06, 0x08, 0x1F, 0x1D), + (0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D), + (0x0D, 0x16, 0x1B, 0x0B, 0x0D, 0x0D, 0x11, 0x10, 0x07, 0x09, 0x1E, 0x1C), + (0x0E, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D), + (0x0F, 0x16, 0x1B, 0x0D, 0x0B, 0x0D, 0x11, 0x10, 0x1C, 0x1E, 0x09, 0x07), + (0x10, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D), + (0x11, 0x0A, 0x1A, 0x0D, 0x0B, 0x0D, 0x11, 0x10, 0x1D, 0x1F, 0x08, 0x06), + (0x12, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D), + (0x14, 0x00, 0x00, 0x11, 0x11), (0x18, 0x99), (0x30, 0x06), + (0x12, 0x36, 0x2C, 0x2E, 0x3C, 0x38, 0x35, 0x35, 0x32, 0x2E, 0x1D, 0x2B, 0x21, 0x16, 0x29,), + (0x13, 0x36, 0x2C, 0x2E, 0x3C, 0x38, 0x35, 0x35, 0x32, 0x2E, 0x1D, 0x2B, 0x21, 0x16, 0x29,), + (0x30, 0x0A), (0x02, 0x4F), (0x0B, 0x40), (0x12, 0x3E), (0x13, 0x78), (0x30, 0x0D), (0x0D, 0x04), + (0x10, 0x0C), (0x11, 0x0C), (0x12, 0x0C), (0x13, 0x0C), (0x30, 0x00), + ], +) diff --git a/esphome/components/mipi_dsi/models/m5stack.py b/esphome/components/mipi_dsi/models/m5stack.py new file mode 100644 index 0000000000..6055c77f8f --- /dev/null +++ b/esphome/components/mipi_dsi/models/m5stack.py @@ -0,0 +1,57 @@ +from esphome.components.mipi import DriverChip +import esphome.config_validation as cv + +# fmt: off +DriverChip( + "M5STACK-TAB5", + height=1280, + width=720, + hsync_back_porch=140, + hsync_pulse_width=40, + hsync_front_porch=40, + vsync_back_porch=20, + vsync_pulse_width=4, + vsync_front_porch=20, + pclk_frequency="60MHz", + lane_bit_rate="730Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0xFF, 0x98, 0x81, 0x01), # Select Page 1 + (0xB7, 0x03), # Pad control - 2 lane + (0xFF, 0x98, 0x81, 0x00), # Select Page 0 + # CMD_Page 3 + (0xFF, 0x98, 0x81, 0x03), # Select Page 3 + (0x01, 0x00), (0x02, 0x00), (0x03, 0x73), (0x04, 0x00), (0x05, 0x00), (0x06, 0x08), (0x07, 0x00), (0x08, 0x00), + (0x09, 0x1B), (0x0A, 0x01), (0x0B, 0x01), (0x0C, 0x0D), (0x0D, 0x01), (0x0E, 0x01), (0x0F, 0x26), (0x10, 0x26), + (0x11, 0x00), (0x12, 0x00), (0x13, 0x02), (0x14, 0x00), (0x15, 0x00), (0x16, 0x00), (0x17, 0x00), (0x18, 0x00), + (0x19, 0x00), (0x1A, 0x00), (0x1B, 0x00), (0x1C, 0x00), (0x1D, 0x00), (0x1E, 0x40), (0x1F, 0x00), (0x20, 0x06), + (0x21, 0x01), (0x22, 0x00), (0x23, 0x00), (0x24, 0x00), (0x25, 0x00), (0x26, 0x00), (0x27, 0x00), (0x28, 0x33), + (0x29, 0x03), (0x2A, 0x00), (0x2B, 0x00), (0x2C, 0x00), (0x2D, 0x00), (0x2E, 0x00), (0x2F, 0x00), (0x30, 0x00), + (0x31, 0x00), (0x32, 0x00), (0x33, 0x00), (0x34, 0x00), (0x35, 0x00), (0x36, 0x00), (0x37, 0x00), (0x38, 0x00), + (0x39, 0x00), (0x3A, 0x00), (0x3B, 0x00), (0x3C, 0x00), (0x3D, 0x00), (0x3E, 0x00), (0x3F, 0x00), (0x40, 0x00), + (0x41, 0x00), (0x42, 0x00), (0x43, 0x00), (0x44, 0x00), (0x50, 0x01), (0x51, 0x23), (0x52, 0x45), (0x53, 0x67), + (0x54, 0x89), (0x55, 0xAB), (0x56, 0x01), (0x57, 0x23), (0x58, 0x45), (0x59, 0x67), (0x5A, 0x89), (0x5B, 0xAB), + (0x5C, 0xCD), (0x5D, 0xEF), (0x5E, 0x11), (0x5F, 0x02), (0x60, 0x00), (0x61, 0x07), (0x62, 0x06), (0x63, 0x0E), + (0x64, 0x0F), (0x65, 0x0C), (0x66, 0x0D), (0x67, 0x02), (0x68, 0x02), (0x69, 0x02), (0x6A, 0x02), (0x6B, 0x02), + (0x6C, 0x02), (0x6D, 0x02), (0x6E, 0x02), (0x6F, 0x02), (0x70, 0x02), (0x71, 0x02), (0x72, 0x02), (0x73, 0x05), + (0x74, 0x01), (0x75, 0x02), (0x76, 0x00), (0x77, 0x07), (0x78, 0x06), (0x79, 0x0E), (0x7A, 0x0F), (0x7B, 0x0C), + (0x7C, 0x0D), (0x7D, 0x02), (0x7E, 0x02), (0x7F, 0x02), (0x80, 0x02), (0x81, 0x02), (0x82, 0x02), (0x83, 0x02), + (0x84, 0x02), (0x85, 0x02), (0x86, 0x02), (0x87, 0x02), (0x88, 0x02), (0x89, 0x05), (0x8A, 0x01), + (0xFF, 0x98, 0x81, 0x04), # Select Page 4 + (0x38, 0x01), (0x39, 0x00), (0x6C, 0x15), (0x6E, 0x1A), (0x6F, 0x25), (0x3A, 0xA4), (0x8D, 0x20), (0x87, 0xBA), (0x3B, 0x98), + (0xFF, 0x98, 0x81, 0x01), # Select Page 1 + (0x22, 0x0A), (0x31, 0x00), (0x50, 0x6B), (0x51, 0x66), (0x53, 0x73), (0x55, 0x8B), (0x60, 0x1B), (0x61, 0x01), (0x62, 0x0C), (0x63, 0x00), + # Gamma P + (0xA0, 0x00), (0xA1, 0x15), (0xA2, 0x1F), (0xA3, 0x13), (0xA4, 0x11), (0xA5, 0x21), (0xA6, 0x17), (0xA7, 0x1B), + (0xA8, 0x6B), (0xA9, 0x1E), (0xAA, 0x2B), (0xAB, 0x5D), (0xAC, 0x19), (0xAD, 0x14), (0xAE, 0x4B), (0xAF, 0x1D), + (0xB0, 0x27), (0xB1, 0x49), (0xB2, 0x5D), (0xB3, 0x39), + # Gamma N + (0xC0, 0x00), (0xC1, 0x01), (0xC2, 0x0C), (0xC3, 0x11), (0xC4, 0x15), (0xC5, 0x28), (0xC6, 0x1B), (0xC7, 0x1C), + (0xC8, 0x62), (0xC9, 0x1C), (0xCA, 0x29), (0xCB, 0x60), (0xCC, 0x16), (0xCD, 0x17), (0xCE, 0x4A), (0xCF, 0x23), + (0xD0, 0x24), (0xD1, 0x4F), (0xD2, 0x5F), (0xD3, 0x39), + # CMD_Page 0 + (0xFF, 0x98, 0x81, 0x00), # Select Page 0 + (0x35,), (0xFE,), + ], +) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py new file mode 100644 index 0000000000..7cfd6f1645 --- /dev/null +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -0,0 +1,105 @@ +from esphome.components.mipi import DriverChip +import esphome.config_validation as cv + +# fmt: off +DriverChip( + "WAVESHARE-P4-NANO-10.1", + height=1280, + width=800, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=30, + pclk_frequency="80MHz", + lane_bit_rate="1.5Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0xE0, 0x00), # select userpage + (0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8), + (0x80, 0x01), # Select number of lanes (2) + (0xE0, 0x01), # select page 1 + (0x00, 0x00), (0x01, 0x38), (0x03, 0x10), (0x04, 0x38), (0x0C, 0x74), (0x17, 0x00), (0x18, 0xAF), (0x19, 0x00), + (0x1A, 0x00), (0x1B, 0xAF), (0x1C, 0x00), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x00), (0x3C, 0x78), + (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0x7F), (0x40, 0x06), (0x41, 0xA0), (0x42, 0x81), (0x43, 0x1E), (0x44, 0x0D), + (0x45, 0x28), (0x55, 0x02), (0x57, 0x69), (0x59, 0x0A), (0x5A, 0x2A), (0x5B, 0x17), (0x5D, 0x7F), (0x5E, 0x6A), + (0x5F, 0x5B), (0x60, 0x4F), (0x61, 0x4A), (0x62, 0x3D), (0x63, 0x41), (0x64, 0x2A), (0x65, 0x44), (0x66, 0x43), + (0x67, 0x44), (0x68, 0x62), (0x69, 0x52), (0x6A, 0x59), (0x6B, 0x4C), (0x6C, 0x48), (0x6D, 0x3A), (0x6E, 0x26), + (0x6F, 0x00), (0x70, 0x7F), (0x71, 0x6A), (0x72, 0x5B), (0x73, 0x4F), (0x74, 0x4A), (0x75, 0x3D), (0x76, 0x41), + (0x77, 0x2A), (0x78, 0x44), (0x79, 0x43), (0x7A, 0x44), (0x7B, 0x62), (0x7C, 0x52), (0x7D, 0x59), (0x7E, 0x4C), + (0x7F, 0x48), (0x80, 0x3A), (0x81, 0x26), (0x82, 0x00), + (0xE0, 0x02), # select page 2 + (0x00, 0x42), (0x01, 0x42), (0x02, 0x40), (0x03, 0x40), (0x04, 0x5E), (0x05, 0x5E), (0x06, 0x5F), (0x07, 0x5F), + (0x08, 0x5F), (0x09, 0x57), (0x0A, 0x57), (0x0B, 0x77), (0x0C, 0x77), (0x0D, 0x47), (0x0E, 0x47), (0x0F, 0x45), + (0x10, 0x45), (0x11, 0x4B), (0x12, 0x4B), (0x13, 0x49), (0x14, 0x49), (0x15, 0x5F), (0x16, 0x41), (0x17, 0x41), + (0x18, 0x40), (0x19, 0x40), (0x1A, 0x5E), (0x1B, 0x5E), (0x1C, 0x5F), (0x1D, 0x5F), (0x1E, 0x5F), (0x1F, 0x57), + (0x20, 0x57), (0x21, 0x77), (0x22, 0x77), (0x23, 0x46), (0x24, 0x46), (0x25, 0x44), (0x26, 0x44), (0x27, 0x4A), + (0x28, 0x4A), (0x29, 0x48), (0x2A, 0x48), (0x2B, 0x5F), (0x2C, 0x01), (0x2D, 0x01), (0x2E, 0x00), (0x2F, 0x00), + (0x30, 0x1F), (0x31, 0x1F), (0x32, 0x1E), (0x33, 0x1E), (0x34, 0x1F), (0x35, 0x17), (0x36, 0x17), (0x37, 0x37), + (0x38, 0x37), (0x39, 0x08), (0x3A, 0x08), (0x3B, 0x0A), (0x3C, 0x0A), (0x3D, 0x04), (0x3E, 0x04), (0x3F, 0x06), + (0x40, 0x06), (0x41, 0x1F), (0x42, 0x02), (0x43, 0x02), (0x44, 0x00), (0x45, 0x00), (0x46, 0x1F), (0x47, 0x1F), + (0x48, 0x1E), (0x49, 0x1E), (0x4A, 0x1F), (0x4B, 0x17), (0x4C, 0x17), (0x4D, 0x37), (0x4E, 0x37), (0x4F, 0x09), + (0x50, 0x09), (0x51, 0x0B), (0x52, 0x0B), (0x53, 0x05), (0x54, 0x05), (0x55, 0x07), (0x56, 0x07), (0x57, 0x1F), + (0x58, 0x40), (0x5B, 0x30), (0x5C, 0x00), (0x5D, 0x34), (0x5E, 0x05), (0x5F, 0x02), (0x63, 0x00), (0x64, 0x6A), + (0x67, 0x73), (0x68, 0x07), (0x69, 0x08), (0x6A, 0x6A), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x00), (0x6E, 0x00), + (0x6F, 0x88), (0x75, 0xFF), (0x77, 0xDD), (0x78, 0x2C), (0x79, 0x15), (0x7A, 0x17), (0x7D, 0x14), (0x7E, 0x82), + (0xE0, 0x04), # select page 4 + (0x00, 0x0E), (0x02, 0xB3), (0x09, 0x61), (0x0E, 0x48), (0x37, 0x58), (0x2B, 0x0F), + (0xE0, 0x00), # Select userpage + (0xE6, 0x02), (0xE7, 0x0C), + ], +) + +DriverChip( + "WAVESHARE-P4-86-PANEL", + height=720, + width=720, + hsync_back_porch=80, + hsync_pulse_width=20, + hsync_front_porch=80, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=30, + pclk_frequency="46MHz", + lane_bit_rate="1Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + reset_pin=27, + initsequence=[ + (0xB9, 0xF1, 0x12, 0x83), + ( + 0xBA, 0x31, 0x81, 0x05, 0xF9, 0x0E, 0x0E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00, + 0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37, + ), + (0xB8, 0x25, 0x22, 0xF0, 0x63), + (0xBF, 0x02, 0x11, 0x00), + (0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00), + (0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00), + (0xBC, 0x46), (0xCC, 0x0B), (0xB4, 0x80), (0xB2, 0x3C, 0x12, 0x30), + (0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x03, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10,), + (0xC1, 0x36, 0x00, 0x32, 0x32, 0x77, 0xF1, 0xCC, 0xCC, 0x77, 0x77, 0x33, 0x33), + (0xB5, 0x0A, 0x0A), + (0xB6, 0xB2, 0xB2), + ( + 0xE9, 0xC8, 0x10, 0x0A, 0x10, 0x0F, 0xA1, 0x80, 0x12, 0x31, 0x23, 0x47, 0x86, 0xA1, 0x80, + 0x47, 0x08, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x48, + 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x48, 0x13, 0x8B, 0xAF, 0x57, + 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ), + ( + 0xEA, 0x96, 0x12, 0x01, 0x01, 0x01, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x31, + 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x4F, 0x20, 0x8B, 0xA8, 0x20, 0x64, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xA1, 0x80, 0x00, 0x00, + 0x00, 0x00, + ), + ( + 0xE0, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, 0x15, 0x14, + 0x15, 0x10, 0x17, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, + 0x15, 0x14, 0x15, 0x10, 0x17, + ), + ], +) diff --git a/esphome/components/mipi_rgb/models/lilygo.py b/esphome/components/mipi_rgb/models/lilygo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml new file mode 100644 index 0000000000..7d1fc84121 --- /dev/null +++ b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml @@ -0,0 +1,28 @@ +esphome: + name: p4-test + +esp32: + board: esp32-p4-evboard + framework: + type: esp-idf + +psram: + speed: 200MHz + +esp_ldo: + - channel: 3 + voltage: 2.5V + +display: + - platform: mipi_dsi + model: WAVESHARE-P4-NANO-10.1 + +i2c: + sda: GPIO7 + scl: GPIO8 + scan: true + frequency: 400kHz + +#light: + #- platform: mipi_dsi + #id: backlight_id diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py new file mode 100644 index 0000000000..f8a9af0279 --- /dev/null +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -0,0 +1,127 @@ +"""Tests for mpi_dsi configuration validation.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome import config_validation as cv +from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32P4 +from esphome.const import ( + CONF_DIMENSIONS, + CONF_HEIGHT, + CONF_INIT_SEQUENCE, + CONF_WIDTH, + KEY_VARIANT, + PlatformFramework, +) +from tests.component_tests.types import SetCoreConfigCallable + + +def test_configuration_errors(set_core_config: SetCoreConfigCallable) -> None: + """Test detection of invalid configuration""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-p4-evboard", KEY_VARIANT: VARIANT_ESP32P4}, + ) + + from esphome.components.mipi_dsi.display import CONFIG_SCHEMA + + with pytest.raises(cv.Invalid, match="expected a dictionary"): + CONFIG_SCHEMA("a string") + + with pytest.raises( + cv.Invalid, match=r"required key not provided @ data\['model'\]" + ): + CONFIG_SCHEMA({"id": "display_id"}) + + with pytest.raises( + cv.Invalid, + match=r"string value is None for dictionary value @ data\['lane_bit_rate'\]", + ): + CONFIG_SCHEMA( + {"id": "display_id", "model": "custom", "init_sequence": [[0x36, 0x01]]} + ) + + with pytest.raises( + cv.Invalid, match=r"required key not provided @ data\['dimensions'\]" + ): + CONFIG_SCHEMA( + { + "id": "display_id", + "model": "custom", + "init_sequence": [[0x36, 0x01]], + "lane_bit_rate": "1.5Gbps", + } + ) + + with pytest.raises( + cv.Invalid, match=r"required key not provided @ data\['init_sequence'\]" + ): + CONFIG_SCHEMA( + { + "model": "custom", + "lane_bit_rate": "1.5Gbps", + "dimensions": {"width": 320, "height": 240}, + } + ) + + +def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None: + """Test successful configuration validation.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-p4-evboard", KEY_VARIANT: VARIANT_ESP32P4}, + ) + + from esphome.components.mipi_dsi.display import CONFIG_SCHEMA, MODELS + + # Custom model with all options + CONFIG_SCHEMA( + { + "model": "custom", + "pixel_mode": "16bit", + "id": "display_id", + "byte_order": "little_endian", + "color_order": "rgb", + "reset_pin": 12, + "init_sequence": [[0xA0, 0x01]], + "dimensions": { + "width": 320, + "height": 240, + }, + "invert_colors": True, + "transform": {"mirror_x": True, "mirror_y": True}, + "pclk_frequency": "40MHz", + "lane_bit_rate": "1.5Gbps", + "lanes": 2, + "use_axis_flips": True, + } + ) + + # Test all models, providing default values where necessary + for name, model in MODELS.items(): + config = {"model": name} + if model.initsequence is None: + config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]] + if not model.get_default(CONF_DIMENSIONS): + config[CONF_DIMENSIONS] = {CONF_WIDTH: 400, CONF_HEIGHT: 300} + if not model.get_default("lane_bit_rate"): + config["lane_bit_rate"] = "1.5Gbps" + CONFIG_SCHEMA(config) + + +def test_code_generation( + generate_main: Callable[[str | Path], str], + component_fixture_path: Callable[[str], Path], +) -> None: + """Test code generation for display.""" + + main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) + assert ( + "mipi_dsi_mipi_dsi_id = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + in main_cpp + ) + assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp + assert "mipi_dsi_mipi_dsi_id->set_lane_bit_rate(1500);" in main_cpp + # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp diff --git a/tests/components/mipi_dsi/test.esp32-p4-idf.yaml b/tests/components/mipi_dsi/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..8a6f3c87ba --- /dev/null +++ b/tests/components/mipi_dsi/test.esp32-p4-idf.yaml @@ -0,0 +1,19 @@ +esp_ldo: + - id: ldo_id + channel: 3 + voltage: 2.5V + +display: + - platform: mipi_dsi + model: JC1060P470 + enable_pin: GPIO22 + +#light: + #- platform: mipi_dsi + #id: backlight_id + +i2c: + sda: GPIO7 + scl: GPIO8 + scan: true + frequency: 400kHz