Merge branch 'optional_api' into integration

This commit is contained in:
J. Nick Koston 2025-07-25 11:02:01 -10:00
commit 88adbe7197
No known key found for this signature in database
20 changed files with 1229 additions and 28 deletions

View File

@ -294,6 +294,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov esphome/components/midea_ir/* @dudanov
esphome/components/mipi_dsi/* @clydebarrow
esphome/components/mipi_spi/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow
esphome/components/mitsubishi/* @RubyBailey esphome/components/mitsubishi/* @RubyBailey
esphome/components/mixer/speaker/* @kahrendt esphome/components/mixer/speaker/* @kahrendt

View File

@ -243,21 +243,7 @@ void APIConnection::loop() {
#endif #endif
if (state_subs_at_ >= 0) { if (state_subs_at_ >= 0) {
const auto &subs = this->parent_->get_state_subs(); this->process_state_subscriptions_();
if (state_subs_at_ < static_cast<int>(subs.size())) {
auto &it = subs[state_subs_at_];
SubscribeHomeAssistantStateResponse resp;
resp.set_entity_id(StringRef(it.entity_id));
// attribute.value() returns temporary - must store it
std::string attribute_value = it.attribute.value();
resp.set_attribute(StringRef(attribute_value));
resp.once = it.once;
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
state_subs_at_++;
}
} else {
state_subs_at_ = -1;
}
} }
} }
@ -642,17 +628,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value()); resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) { if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) {
// custom_fan_mode.value() returns temporary - must store it resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value()));
std::string custom_fan_mode = climate->custom_fan_mode.value();
resp.set_custom_fan_mode(StringRef(custom_fan_mode));
} }
if (traits.get_supports_presets() && climate->preset.has_value()) { if (traits.get_supports_presets() && climate->preset.has_value()) {
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
} }
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) {
// custom_preset.value() returns temporary - must store it resp.set_custom_preset(StringRef(climate->custom_preset.value()));
std::string custom_preset = climate->custom_preset.value();
resp.set_custom_preset(StringRef(custom_preset));
} }
if (traits.get_supports_swing_modes()) if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
@ -1836,5 +1818,27 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::process_state_subscriptions_() {
const auto &subs = this->parent_->get_state_subs();
if (this->state_subs_at_ >= static_cast<int>(subs.size())) {
this->state_subs_at_ = -1;
return;
}
const auto &it = subs[this->state_subs_at_];
SubscribeHomeAssistantStateResponse resp;
resp.set_entity_id(StringRef(it.entity_id));
// Avoid string copy by directly using the optional's value if it exists
if (it.attribute.has_value()) {
resp.set_attribute(StringRef(it.attribute.value()));
}
resp.once = it.once;
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
this->state_subs_at_++;
}
}
} // namespace esphome::api } // namespace esphome::api
#endif #endif

View File

@ -288,6 +288,9 @@ class APIConnection : public APIServerConnection {
// Helper function to handle authentication completion // Helper function to handle authentication completion
void complete_authentication_(); void complete_authentication_();
// Process state subscriptions efficiently
void process_state_subscriptions_();
// Non-template helper to encode any ProtoMessage // Non-template helper to encode any ProtoMessage
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single); uint32_t remaining_size, bool is_single);

View File

@ -35,11 +35,10 @@ namespace esphome::api {
* *
* Unsafe Patterns (WILL cause crashes/corruption): * Unsafe Patterns (WILL cause crashes/corruption):
* 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value * 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value
* 2. Optional values: msg.set_field(StringRef(optional.value())) // value() returns a copy * 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
* 3. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
* *
* For unsafe patterns, store in a local variable first: * For unsafe patterns, store in a local variable first:
* std::string temp = optional.value(); // or get_string() or str1 + str2 * std::string temp = get_string(); // or str1 + str2
* msg.set_field(StringRef(temp)); * msg.set_field(StringRef(temp));
* *
* The send_*_response pattern ensures proper lifetime management by encoding * The send_*_response pattern ensures proper lifetime management by encoding

View File

@ -230,7 +230,7 @@ class DriverChip:
): ):
name = name.upper() name = name.upper()
self.name = name self.name = name
self.initsequence = initsequence self.initsequence = initsequence or defaults.get("init_sequence")
self.defaults = defaults self.defaults = defaults
DriverChip.models[name] = self DriverChip.models[name] = self
@ -347,7 +347,7 @@ class DriverChip:
Pixel format, color order, and orientation will be set. Pixel format, color order, and orientation will be set.
Returns a tuple of the init sequence and the computed MADCTL value. 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, []) custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
sequence.extend(custom_sequence) sequence.extend(custom_sequence)
# Ensure each command is a tuple # Ensure each command is a tuple
@ -356,6 +356,8 @@ class DriverChip:
# Set pixel format if not already in the custom sequence # Set pixel format if not already in the custom sequence
pixel_mode = config[CONF_PIXEL_MODE] pixel_mode = config[CONF_PIXEL_MODE]
if not isinstance(pixel_mode, int): if not isinstance(pixel_mode, int):
if not pixel_mode.endswith("bit"):
pixel_mode = f"{pixel_mode}bit"
pixel_mode = PIXEL_MODES[pixel_mode] pixel_mode = PIXEL_MODES[pixel_mode]
sequence.append((PIXFMT, pixel_mode)) sequence.append((PIXFMT, pixel_mode))

View File

@ -0,0 +1,5 @@
import esphome.codegen as cg
CODEOWNERS = ["@clydebarrow"]
mipi_dsi_ns = cg.esphome_ns.namespace("mipi_dsi")

View File

@ -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_))

View File

@ -0,0 +1,379 @@
#ifdef USE_ESP32_VARIANT_ESP32P4
#include <utility>
#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<SemaphoreHandle_t *>(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<uint8_t> 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<uint16_t *>(this->buffer_);
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
uint8_t lo_byte = static_cast<uint8_t>((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<uint16_t *>(this->buffer_);
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
uint8_t lo_byte = static_cast<uint8_t>((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

View File

@ -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<GPIOPin *> 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<uint8_t> &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<GPIOPin *> 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<uint8_t> 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

View File

@ -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),
],
)

View File

@ -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,),
],
)

View File

@ -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,
),
],
)

View File

@ -106,7 +106,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
// Calculate random offset (0 to min(interval/2, 5s)) // Calculate random offset (0 to min(interval/2, 5s))
uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float());
item->next_execution_ = now + offset; item->next_execution_ = now + offset;
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr, delay, offset); ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr ? name_cstr : "", delay,
offset);
} else { } else {
item->interval = 0; item->interval = 0;
item->next_execution_ = now + delay; item->next_execution_ = now + delay;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -210,6 +210,15 @@ sensor:
name: "Test Sensor 50" name: "Test Sensor 50"
lambda: return 50.0; lambda: return 50.0;
update_interval: 0.1s update_interval: 0.1s
# Temperature sensor for the thermostat
- platform: template
name: "Temperature Sensor"
id: temp_sensor
lambda: return 22.5;
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
update_interval: 5s
# Mixed entity types for comprehensive batching test # Mixed entity types for comprehensive batching test
binary_sensor: binary_sensor:
@ -285,6 +294,50 @@ valve:
stop_action: stop_action:
- logger.log: "Valve stopping" - logger.log: "Valve stopping"
output:
- platform: template
id: heater_output
type: binary
write_action:
- logger.log: "Heater output changed"
- platform: template
id: cooler_output
type: binary
write_action:
- logger.log: "Cooler output changed"
climate:
- platform: thermostat
name: "Test Thermostat"
sensor: temp_sensor
default_preset: Home
on_boot_restore_from: default_preset
min_heating_off_time: 1s
min_heating_run_time: 1s
min_cooling_off_time: 1s
min_cooling_run_time: 1s
min_idle_time: 1s
heat_action:
- output.turn_on: heater_output
cool_action:
- output.turn_on: cooler_output
idle_action:
- output.turn_off: heater_output
- output.turn_off: cooler_output
preset:
- name: Home
default_target_temperature_low: 20
default_target_temperature_high: 24
mode: heat_cool
- name: Away
default_target_temperature_low: 16
default_target_temperature_high: 26
mode: heat_cool
- name: Sleep
default_target_temperature_low: 18
default_target_temperature_high: 22
mode: heat_cool
alarm_control_panel: alarm_control_panel:
- platform: template - platform: template
name: "Test Alarm" name: "Test Alarm"

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
from aioesphomeapi import EntityState, SensorState from aioesphomeapi import ClimateInfo, ClimateState, EntityState, SensorState
import pytest import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction from .types import APIClientConnectedFactory, RunCompiledFunction
@ -70,3 +70,28 @@ async def test_host_mode_many_entities(
assert len(sensor_states) >= 50, ( assert len(sensor_states) >= 50, (
f"Expected at least 50 sensor states, got {len(sensor_states)}" f"Expected at least 50 sensor states, got {len(sensor_states)}"
) )
# Verify we received the climate entity
climate_states = [s for s in states.values() if isinstance(s, ClimateState)]
assert len(climate_states) >= 1, (
f"Expected at least 1 climate state, got {len(climate_states)}"
)
# Get entity info to verify climate entity details
entities = await client.list_entities_services()
climate_infos = [e for e in entities[0] if isinstance(e, ClimateInfo)]
assert len(climate_infos) >= 1, "Expected at least 1 climate entity"
climate_info = climate_infos[0]
# Verify the thermostat has presets
assert len(climate_info.supported_presets) > 0, (
"Expected climate to have presets"
)
# The thermostat platform uses standard presets (Home, Away, Sleep)
# which should be transmitted properly without string copies
# Verify specific presets exist
preset_names = [p.name for p in climate_info.supported_presets]
assert "HOME" in preset_names, f"Expected 'HOME' preset, got {preset_names}"
assert "AWAY" in preset_names, f"Expected 'AWAY' preset, got {preset_names}"
assert "SLEEP" in preset_names, f"Expected 'SLEEP' preset, got {preset_names}"