[mipi_spi] Rotation and buffer size changes (#15047)

This commit is contained in:
Clyde Stubbs
2026-04-03 11:28:45 +10:00
committed by GitHub
parent 710186998b
commit af662da90d
18 changed files with 557 additions and 224 deletions
+2
View File
@@ -32,4 +32,6 @@ ICON_CURRENT_DC = "mdi:current-dc"
ICON_SOLAR_PANEL = "mdi:solar-panel"
ICON_SOLAR_POWER = "mdi:solar-power"
KEY_METADATA = "metadata"
UNIT_AMPERE_HOUR = "Ah"
+38
View File
@@ -1,6 +1,9 @@
from dataclasses import dataclass
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components.const import KEY_METADATA
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
@@ -16,7 +19,9 @@ from esphome.const import (
SCHEDULER_DONT_RUN,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj
DOMAIN = "display"
IS_PLATFORM_COMPONENT = True
display_ns = cg.esphome_ns.namespace("display")
@@ -146,6 +151,39 @@ async def setup_display_core_(var, config):
cg.add(var.show_test_card())
# Storage of display metadata in a central location, accessible via the id
@dataclass(frozen=True)
class DisplayMetaData:
width: int = 0
height: int = 0
has_writer: bool = False
has_hardware_rotation: bool = False
def get_all_display_metadata() -> dict[str, DisplayMetaData]:
"""Get all display metadata."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
def get_display_metadata(display_id: str) -> DisplayMetaData | None:
"""Get display metadata by ID for use by other components."""
return get_all_display_metadata().get(display_id, DisplayMetaData())
def add_metadata(
id: str | MockObj,
width: int,
height: int,
has_writer: bool,
has_hardware_rotation: bool = False,
):
get_all_display_metadata()[str(id)] = DisplayMetaData(
width, height, has_writer, has_hardware_rotation
)
async def register_display(var, config):
await cg.register_component(var, config)
await setup_display_core_(var, config)
+1 -1
View File
@@ -704,7 +704,7 @@ class Display : public PollingComponent {
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
/// Internal method to set the display rotation with.
void set_rotation(DisplayRotation rotation);
virtual void set_rotation(DisplayRotation rotation);
// Internal method to set display auto clearing.
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
+1 -2
View File
@@ -12,7 +12,7 @@ from PIL import Image, UnidentifiedImageError
from esphome import core, external_files
import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER
from esphome.components.const import CONF_BYTE_ORDER, KEY_METADATA
import esphome.config_validation as cv
from esphome.const import (
CONF_DEFAULTS,
@@ -53,7 +53,6 @@ CONF_CHROMA_KEY = "chroma_key"
CONF_ALPHA_CHANNEL = "alpha_channel"
CONF_INVERT_ALPHA = "invert_alpha"
CONF_IMAGES = "images"
KEY_METADATA = "metadata"
TRANSPARENCY_TYPES = (
CONF_OPAQUE,
+47 -23
View File
@@ -128,6 +128,8 @@ MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left
MADCTL_XFLIP = 0x02 # Mirror the display horizontally
MADCTL_YFLIP = 0x01 # Mirror the display vertically
MADCTL_FLIP_FLAG = 0x100 # meta-flag to indicate use of axis flips
# Special constant for delays in command sequences
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
@@ -329,7 +331,13 @@ class DriverChip:
return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms
return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms
def get_dimensions(self, config) -> tuple[int, int, int, int]:
def get_dimensions(self, config, swap: bool = True) -> tuple[int, int, int, int]:
"""
Return the dimensions of the current model.
:param config: The current configuration
:param swap: If width/height should be swapped when axes are swapped.
:return:
"""
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
@@ -361,13 +369,12 @@ class DriverChip:
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer
if transform.get(CONF_SWAP_XY) is True:
if swap and transform.get(CONF_SWAP_XY) is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
return width, height, offset_width, offset_height
def get_transform(self, config) -> dict[str, bool]:
can_transform = self.rotation_as_transform(config)
def get_base_transform(self, config):
transform = config.get(
CONF_TRANSFORM,
{
@@ -376,14 +383,20 @@ class DriverChip:
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
},
)
if not isinstance(transform, dict):
# Presumably disabled
return {
CONF_MIRROR_X: False,
CONF_MIRROR_Y: False,
CONF_SWAP_XY: False,
CONF_TRANSFORM: False,
}
if isinstance(transform, dict):
return transform
# Transform is disabled
return {
CONF_MIRROR_X: False,
CONF_MIRROR_Y: False,
CONF_SWAP_XY: False,
CONF_TRANSFORM: False,
}
def get_transform(self, config) -> dict[str, bool]:
transform = self.get_base_transform(config)
can_transform = self.rotation_as_transform(config)
# Can we use the MADCTL register to set the rotation?
if can_transform and CONF_TRANSFORM not in config:
rotation = config[CONF_ROTATION]
@@ -411,11 +424,15 @@ class DriverChip:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def add_madctl(self, sequence: list, config: dict):
# Add the MADCTL command to the sequence based on the configuration.
use_flip = config.get(CONF_USE_AXIS_FLIPS)
madctl = 0
transform = self.get_transform(config)
def get_madctl(self, transform: dict, config: dict) -> int:
"""
Convert a transform to MADCTL bits
:param transform: The transform dict
:param use_flip: Whether to use axis flips
:return: MADCTL value
"""
use_flip = config.get(CONF_USE_AXIS_FLIPS, False)
madctl = MADCTL_FLIP_FLAG if use_flip else 0
if transform[CONF_MIRROR_X]:
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
if transform[CONF_MIRROR_Y]:
@@ -424,22 +441,28 @@ class DriverChip:
madctl |= MADCTL_MV
if config[CONF_COLOR_ORDER] == MODE_BGR:
madctl |= MADCTL_BGR
sequence.append((MADCTL, madctl))
return madctl
def add_madctl(self, sequence: list, config: dict):
# Add the MADCTL command to the sequence based on the configuration.
# This takes into account rotation if it can be implemented in the transform
transform = self.get_transform(config)
madctl = self.get_madctl(transform, config)
sequence.append((MADCTL, madctl & 0xFF))
def skip_command(self, command: str):
"""
Allow suppressing a standard command in the init sequence.
"""
return self.get_default(f"no_{command.lower()}", False)
def get_sequence(self, config) -> tuple[tuple[int, ...], int]:
def get_sequence(self, config, add_madctl=True) -> tuple[int, ...]:
"""
Create the init sequence for the display.
Use the default sequence from the model, if any, and append any custom sequence provided in the config.
Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence
Pixel format, color order, and orientation will be set.
Returns a tuple of the init sequence and the computed MADCTL value.
MADCTL will be set if add_madctl is True
Returns the init sequence
"""
sequence = list(self.initsequence or ())
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
@@ -457,7 +480,8 @@ class DriverChip:
if self.rotation_as_transform(config):
LOGGER.info("Using hardware transform to implement rotation")
madctl = self.add_madctl(sequence, config)
if add_madctl:
self.add_madctl(sequence, config)
if config[CONF_INVERT_COLORS]:
sequence.append((INVON,))
else:
@@ -471,7 +495,7 @@ class DriverChip:
# Flatten the sequence into a list of bytes, with the length of each command
# or the delay flag inserted where needed
return flatten_sequence(sequence), madctl
return flatten_sequence(sequence)
def requires_buffer(config) -> bool:
+1 -2
View File
@@ -192,10 +192,9 @@ async def to_code(config):
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)
sequence = 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]))
+3 -9
View File
@@ -392,9 +392,6 @@ void MIPI_DSI::dump_config() {
"\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: %.0fMbps"
@@ -406,14 +403,11 @@ void MIPI_DSI::dump_config() {
"\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: %.1fMHz",
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",
this->model_, this->width_, this->height_, 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_,
YESNO(this->invert_colors_), this->pclk_frequency_);
LOG_PIN(" Reset Pin ", this->reset_pin_);
}
-2
View File
@@ -60,7 +60,6 @@ class MIPI_DSI : public display::Display {
void set_model(const char *model) { this->model_ = model; }
void set_lane_bit_rate(float 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 LogString *message, esp_err_t err);
@@ -86,7 +85,6 @@ class MIPI_DSI : public display::Display {
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;
+1 -2
View File
@@ -265,9 +265,8 @@ async def to_code(config):
if CONF_SPI_ID in config:
await spi.register_spi_device(var, config, write_only=True)
sequence, madctl = model.get_sequence(config)
sequence = model.get_sequence(config)
cg.add(var.set_init_sequence(sequence))
cg.add(var.set_madctl(madctl))
cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
+1 -9
View File
@@ -118,15 +118,7 @@ void MipiRgbSpi::dump_config() {
MipiRgb::dump_config();
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
ESP_LOGCONFIG(TAG,
" SPI Data rate: %uMHz"
"\n Mirror X: %s"
"\n Mirror Y: %s"
"\n Swap X/Y: %s"
"\n Color Order: %s",
(unsigned) (this->data_rate_ / 1000000), YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)),
YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY | MADCTL_ML)), YESNO(this->madctl_ & MADCTL_MV),
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB");
ESP_LOGCONFIG(TAG, " SPI Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000));
}
#endif // USE_SPI
-2
View File
@@ -38,7 +38,6 @@ class MipiRgb : public display::Display {
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; }
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; };
void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; }
@@ -84,7 +83,6 @@ class MipiRgb : public display::Display {
uint16_t vsync_front_porch_ = 10;
uint32_t pclk_frequency_ = 16 * 1000 * 1000;
bool pclk_inverted_{true};
uint8_t madctl_{};
const char *model_{"Unknown"};
bool invert_colors_{};
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
+35 -87
View File
@@ -10,7 +10,7 @@ from esphome.components.const import (
CONF_COLOR_DEPTH,
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.mipi import (
CONF_PIXEL_MODE,
CONF_USE_AXIS_FLIPS,
@@ -47,12 +47,10 @@ from esphome.const import (
CONF_MIRROR_Y,
CONF_MODEL,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_WIDTH,
)
from esphome.core import CORE
from esphome.cpp_generator import TemplateArguments
from esphome.final_validate import full_config
@@ -113,22 +111,21 @@ DISPLAY_PIXEL_MODES = {
def denominator(config):
"""
Calculate the best denominator for a buffer size fraction.
The denominator must be a number between 2 and 16 that divides the display height evenly,
The denominator should be a number between 2 and 16 that divides the display height evenly,
and the fraction represented by the denominator must be less than or equal to the given fraction.
:config: The configuration dictionary containing the buffer size fraction and display dimensions
:return: The denominator to use for the buffer size fraction
"""
model = MODELS[config[CONF_MODEL]]
frac = config.get(CONF_BUFFER_SIZE)
if frac is None or frac > 0.75:
_width, height, _offset_width, _offset_height = model.get_dimensions(config)
if frac is None or frac > 0.75 or height < 32:
return 1
height, _width, _offset_width, _offset_height = model.get_dimensions(config)
try:
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
except StopIteration:
raise cv.Invalid(
f"Buffer size fraction {frac} is not compatible with display height {height}"
) from StopIteration
# No exact divisor, just use the closest.
return next(x for x in range(2, 17) if frac >= 1 / x)
def model_schema(config):
@@ -287,30 +284,19 @@ def _final_validate(config):
config[CONF_SHOW_TEST_CARD] = True
if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
if not requires_buffer(config):
return config # No buffer needed, so no need to set a buffer size
# If PSRAM is not enabled, choose a small buffer size by default
if not requires_buffer(config):
# not our problem.
return config
return config # No buffer needed, so no need to set a buffer size
color_depth = get_color_depth(config)
frac = denominator(config)
height, width, _offset_width, _offset_height = model.get_dimensions(config)
width, height, _offset_width, _offset_height = model.get_dimensions(config)
buffer_size = color_depth // 8 * width * height // frac
# Target a buffer size of 20kB
fraction = 20000.0 / buffer_size
try:
config[CONF_BUFFER_SIZE] = 1.0 / next(
x for x in range(2, 17) if fraction >= 1 / x and height % x == 0
)
except StopIteration:
# Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0
# PSRAM will be needed.
if CORE.is_esp32:
raise cv.Invalid(
"PSRAM is required for this display"
) from StopIteration
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
fraction = min(20000.0, buffer_size // 16) / buffer_size
config[CONF_BUFFER_SIZE] = 1.0 / next(
x for x in range(2, 17) if fraction >= 1 / x
)
return config
@@ -318,39 +304,6 @@ def _final_validate(config):
FINAL_VALIDATE_SCHEMA = _final_validate
def get_transform(config):
"""
Get the transformation configuration for the display.
:param config:
:return:
"""
model = MODELS[config[CONF_MODEL]]
can_transform = model.rotation_as_transform(config)
transform = config.get(
CONF_TRANSFORM,
{
CONF_MIRROR_X: model.get_default(CONF_MIRROR_X, False),
CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y, False),
CONF_SWAP_XY: model.get_default(CONF_SWAP_XY, False),
},
)
# Can we use the MADCTL register to set the rotation?
if can_transform and CONF_TRANSFORM not in config:
rotation = config[CONF_ROTATION]
if rotation == 180:
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
elif rotation == 90:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
else:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
transform[CONF_TRANSFORM] = True
return transform
def get_instance(config):
"""
Get the type of MipiSpi instance to create based on the configuration,
@@ -359,7 +312,16 @@ def get_instance(config):
:return: type, template arguments
"""
model = MODELS[config[CONF_MODEL]]
width, height, offset_width, offset_height = model.get_dimensions(config)
has_hardware_transform = config.get(
CONF_TRANSFORM
) != CONF_DISABLED and model.transforms == {
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_SWAP_XY,
}
width, height, offset_width, offset_height = model.get_dimensions(
config, not has_hardware_transform
)
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
bufferpixels = COLOR_DEPTHS[color_depth]
@@ -373,57 +335,43 @@ def get_instance(config):
bus_type = BusTypes[bus_type]
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
frac = denominator(config)
rotation = (
0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0)
)
madctl = model.get_madctl(model.get_base_transform(config), config)
has_writer = requires_buffer(config)
templateargs = [
buffer_type,
bufferpixels,
config[CONF_BYTE_ORDER] == "big_endian",
display_pixel_mode,
bus_type,
width,
height,
offset_width,
offset_height,
madctl,
has_hardware_transform,
]
display.add_metadata(
config[CONF_ID], width, height, has_writer, has_hardware_transform
)
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
if requires_buffer(config):
templateargs.extend(
[
width,
height,
offset_width,
offset_height,
DISPLAY_ROTATIONS[rotation],
frac,
config[CONF_DRAW_ROUNDING],
]
)
return MipiSpiBuffer, templateargs
# Swap height and width if the display is rotated 90 or 270 degrees in software
if rotation in (90, 270):
width, height = height, width
offset_width, offset_height = offset_height, offset_width
templateargs.extend(
[
width,
height,
offset_width,
offset_height,
]
)
return MipiSpi, templateargs
async def to_code(config):
model = MODELS[config[CONF_MODEL]]
var_id = config[CONF_ID]
init_sequence = model.get_sequence(config, False)
var_id.type, templateargs = get_instance(config)
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
init_sequence, _madctl = model.get_sequence(config)
cg.add(var.set_init_sequence(init_sequence))
if model.rotation_as_transform(config):
if CONF_TRANSFORM in config:
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
else:
config[CONF_ROTATION] = 0
cg.add(var.set_model(config[CONF_MODEL]))
if enable_pin := config.get(CONF_ENABLE_PIN):
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
+6 -4
View File
@@ -5,7 +5,8 @@ namespace esphome::mipi_spi {
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) {
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width,
bool has_hardware_rotation) {
ESP_LOGCONFIG(TAG,
"MIPI_SPI Display\n"
" Model: %s\n"
@@ -14,6 +15,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s\n"
" Hardware rotation: %s\n"
" Invert colors: %s\n"
" Color order: %s\n"
" Display pixels: %d bits\n"
@@ -22,9 +24,9 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
" SPI Data rate: %uMHz\n"
" SPI Bus width: %d",
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB",
display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast<unsigned>(data_rate / 1000000),
bus_width);
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(has_hardware_rotation), YESNO(invert_colors),
(madctl & MADCTL_BGR) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode,
static_cast<unsigned>(data_rate / 1000000), bus_width);
LOG_PIN(" CS Pin: ", cs);
LOG_PIN(" Reset Pin: ", reset);
LOG_PIN(" DC Pin: ", dc);
+138 -74
View File
@@ -34,13 +34,14 @@ static constexpr uint8_t SWIRE1 = 0x5A;
static constexpr uint8_t SWIRE2 = 0x5B;
static constexpr uint8_t PAGESEL = 0xFE;
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static constexpr uint16_t MADCTL_FLIP_FLAG = 0x100; // controller uses axis flip bits
static constexpr uint8_t DELAY_FLAG = 0xFF;
// store a 16 bit value in a buffer, big endian.
@@ -66,7 +67,8 @@ enum BusType {
// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width);
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width,
bool has_hardware_rotation);
/**
* Base class for MIPI SPI displays.
@@ -83,7 +85,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
* buffer
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT>
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL, bool HAS_HARDWARE_ROTATION>
class MipiSpi : public display::Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_1MHZ> {
@@ -103,10 +105,39 @@ class MipiSpi : public display::Display,
this->brightness_ = brightness;
this->reset_params_();
}
void set_rotation(display::DisplayRotation rotation) override {
this->rotation_ = rotation;
if constexpr (HAS_HARDWARE_ROTATION) {
this->reset_params_();
}
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
int get_width_internal() override { return WIDTH; }
int get_height_internal() override { return HEIGHT; }
int get_width() override {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return HEIGHT;
return WIDTH;
}
int get_height() override {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return WIDTH;
return HEIGHT;
}
// If hardware rotation is in use, the actual display width/height changes with rotation
int get_width_internal() override {
if constexpr (HAS_HARDWARE_ROTATION)
return get_width();
return WIDTH;
}
int get_height_internal() override {
if constexpr (HAS_HARDWARE_ROTATION)
return get_height();
return HEIGHT;
}
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
// reset the display, and write the init sequence
@@ -166,9 +197,6 @@ class MipiSpi : public display::Display,
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
@@ -177,13 +205,13 @@ class MipiSpi : public display::Display,
break;
}
const auto *ptr = vec.data() + index;
esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
this->reset_params_();
// init sequence no longer needed
this->init_sequence_.clear();
}
@@ -206,9 +234,10 @@ class MipiSpi : public display::Display,
}
void dump_config() override {
internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_,
DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_,
this->mode_, this->data_rate_, BUS_TYPE);
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
HAS_HARDWARE_ROTATION);
}
protected:
@@ -219,10 +248,13 @@ class MipiSpi : public display::Display,
// Writes a command to the display, with the given bytes.
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
#endif
// Don't spam the log after setup
if (this->init_sequence_.empty()) {
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
} else {
esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
}
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
@@ -271,16 +303,60 @@ class MipiSpi : public display::Display,
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
if (this->brightness_.has_value())
this->write_command_(BRIGHTNESS, this->brightness_.value());
// calculate new madctl value from base value adjusted for rotation
uint8_t madctl = MADCTL; // lower 8 bits only
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
if constexpr (HAS_HARDWARE_ROTATION) {
switch (this->rotation_) {
default:
break;
case display::DISPLAY_ROTATION_90_DEGREES:
madctl ^= x_mask; // flip X axis
madctl ^= MADCTL_MV; // swap X and Y axes
break;
case display::DISPLAY_ROTATION_180_DEGREES:
madctl ^= x_mask; // flip X axis
madctl ^= y_mask; // flip Y axis
break;
case display::DISPLAY_ROTATION_270_DEGREES:
madctl ^= y_mask; // flip Y axis
madctl ^= MADCTL_MV; // swap X and Y axes
break;
}
}
esph_log_d(TAG, "Setting MADCTL for rotation %d, value %X", this->rotation_, madctl);
this->write_command_(MADCTL_CMD, madctl);
}
uint16_t get_offset_width_() {
if constexpr (HAS_HARDWARE_ROTATION) {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return OFFSET_HEIGHT;
}
return OFFSET_WIDTH;
}
uint16_t get_offset_height_() {
if constexpr (HAS_HARDWARE_ROTATION) {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return OFFSET_WIDTH;
}
return OFFSET_HEIGHT;
}
// set the address window for the next data write
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
uint8_t buf[4];
x1 += OFFSET_WIDTH;
x2 += OFFSET_WIDTH;
y1 += OFFSET_HEIGHT;
y2 += OFFSET_HEIGHT;
x1 += get_offset_width_();
x2 += get_offset_width_();
y1 += get_offset_height_();
y2 += get_offset_height_();
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
@@ -408,7 +484,6 @@ class MipiSpi : public display::Display,
optional<uint8_t> brightness_{};
const char *model_{"Unknown"};
std::vector<uint8_t> init_sequence_{};
uint8_t madctl_{};
};
/**
@@ -427,22 +502,21 @@ class MipiSpi : public display::Display,
* @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even)
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION,
int FRACTION, unsigned ROUNDING>
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL,
bool HAS_HARDWARE_ROTATION, int FRACTION, unsigned ROUNDING>
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
OFFSET_WIDTH, OFFSET_HEIGHT> {
OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION> {
public:
// these values define the buffer size needed to write in accordance with the chip pixel alignment
// requirements. If the required rounding does not divide the width and height, we round up to the next multiple and
// ignore the extra columns and rows when drawing, but use them to write to the display.
static constexpr unsigned BUFFER_WIDTH = (WIDTH + ROUNDING - 1) / ROUNDING * ROUNDING;
static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING;
static constexpr size_t round_buffer(size_t size) { return (size + ROUNDING - 1) / ROUNDING * ROUNDING; }
MipiSpiBuffer() { this->rotation_ = ROTATION; }
MipiSpiBuffer() = default;
void dump_config() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::dump_config();
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
MADCTL, HAS_HARDWARE_ROTATION>::dump_config();
esph_log_config(TAG,
" Rotation: %d°\n"
" Buffer pixels: %d bits\n"
@@ -450,14 +524,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
" Buffer bytes: %zu\n"
" Draw rounding: %u",
this->rotation_, BUFFERPIXEL * 8, FRACTION,
sizeof(BUFFERTYPE) * BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION, ROUNDING);
sizeof(BUFFERTYPE) * round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION, ROUNDING);
}
void setup() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::setup();
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
MADCTL, HAS_HARDWARE_ROTATION>::setup();
RAMAllocator<BUFFERTYPE> allocator{};
this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION);
this->buffer_ = allocator.allocate(round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION);
if (this->buffer_ == nullptr) {
this->mark_failed(LOG_STR("Buffer allocation failed"));
}
@@ -472,11 +546,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
}
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
// the display height,
for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) {
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
this->start_line_ += this->get_height_internal() / FRACTION) {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto lap = millis();
#endif
this->end_line_ = this->start_line_ + HEIGHT / FRACTION;
this->end_line_ =
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
if (this->auto_clear_enabled_) {
this->clear();
}
@@ -503,10 +579,10 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
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->start_line_, BUFFER_WIDTH - w);
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
// invalidate watermarks
this->x_low_ = WIDTH;
this->y_low_ = HEIGHT;
this->x_low_ = this->get_width_internal();
this->y_low_ = this->get_height_internal();
this->x_high_ = 0;
this->y_high_ = 0;
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
@@ -523,10 +599,23 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
void draw_pixel_at(int x, int y, Color color) override {
if (!this->get_clipping().inside(x, y))
return;
rotate_coordinates(x, y);
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
if constexpr (not HAS_HARDWARE_ROTATION) {
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
} else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) {
auto tmp = x;
x = WIDTH - y - 1;
y = tmp;
} else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) {
auto tmp = y;
y = HEIGHT - x - 1;
x = tmp;
}
}
if (x < 0 || x >= this->get_width_internal() || y < this->start_line_ || y >= this->end_line_)
return;
this->buffer_[(y - this->start_line_) * BUFFER_WIDTH + x] = convert_color(color);
this->buffer_[(y - this->start_line_) * round_buffer(this->get_width_internal()) + x] = convert_color(color);
if (x < this->x_low_) {
this->x_low_ = x;
}
@@ -551,39 +640,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
this->x_low_ = 0;
this->y_low_ = this->start_line_;
this->x_high_ = WIDTH - 1;
this->x_high_ = this->get_width_internal() - 1;
this->y_high_ = this->end_line_ - 1;
std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color));
}
int get_width() override {
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
return HEIGHT;
return WIDTH;
}
int get_height() override {
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
return WIDTH;
return HEIGHT;
std::fill_n(this->buffer_, (this->end_line_ - this->start_line_) * round_buffer(this->get_width_internal()),
convert_color(color));
}
protected:
// Rotate the coordinates to match the display orientation.
static void rotate_coordinates(int &x, int &y) {
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) {
auto tmp = x;
x = WIDTH - y - 1;
y = tmp;
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) {
auto tmp = y;
y = HEIGHT - x - 1;
x = tmp;
}
}
// Convert a color to the buffer pixel format.
static BUFFERTYPE convert_color(const Color &color) {
@@ -0,0 +1,81 @@
"""Tests for display component metadata functions."""
from unittest.mock import patch
from esphome.components.display import (
DisplayMetaData,
add_metadata,
get_all_display_metadata,
get_display_metadata,
)
from esphome.cpp_generator import MockObj
def test_add_metadata_with_string_id():
"""Test adding metadata with a plain string ID."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata("my_display", 320, 240, True)
meta = get_display_metadata("my_display")
assert meta == DisplayMetaData(
width=320, height=240, has_writer=True, has_hardware_rotation=False
)
def test_add_metadata_with_mockobj_id():
"""Test adding metadata with a MockObj ID (converted via str())."""
with patch("esphome.components.display.CORE.data", {}):
mock_id = MockObj("my_display_obj")
add_metadata(mock_id, 480, 320, False, has_hardware_rotation=True)
meta = get_display_metadata("my_display_obj")
assert meta == DisplayMetaData(
width=480, height=320, has_writer=False, has_hardware_rotation=True
)
def test_add_metadata_hardware_rotation_default():
"""Test that has_hardware_rotation defaults to False."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata("disp", 128, 64, False)
meta = get_display_metadata("disp")
assert meta.has_hardware_rotation is False
def test_get_display_metadata_missing_returns_none():
"""Test that querying a non-existent ID returns None."""
with patch("esphome.components.display.CORE.data", {}):
data = get_display_metadata("no_such_display")
assert data.width == 0
assert data.height == 0
assert data.has_writer is False
assert data.has_hardware_rotation is False
def test_add_multiple_displays():
"""Test adding metadata for multiple displays."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata("disp_a", 320, 240, True)
add_metadata("disp_b", 128, 64, False, has_hardware_rotation=True)
all_meta = get_all_display_metadata()
assert len(all_meta) == 2
assert all_meta["disp_a"] == DisplayMetaData(320, 240, True, False)
assert all_meta["disp_b"] == DisplayMetaData(128, 64, False, True)
def test_add_metadata_overwrites_existing():
"""Test that adding metadata for the same ID overwrites the previous entry."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata("disp", 320, 240, True)
add_metadata("disp", 640, 480, False, has_hardware_rotation=True)
meta = get_display_metadata("disp")
assert meta == DisplayMetaData(640, 480, False, True)
def test_metadata_is_frozen():
"""Test that DisplayMetaData instances are immutable (frozen dataclass)."""
meta = DisplayMetaData(320, 240, True, False)
try:
meta.width = 640
assert False, "Expected FrozenInstanceError"
except AttributeError:
pass
@@ -0,0 +1,200 @@
"""Tests for display metadata created by mipi_spi component."""
from collections.abc import Callable
from pathlib import Path
from esphome.components.display import (
DisplayMetaData,
get_all_display_metadata,
get_display_metadata,
)
from esphome.components.esp32 import (
KEY_BOARD,
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32S3,
)
from esphome.components.mipi_spi.display import (
CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA,
get_instance,
)
from esphome.const import PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
def validated_config(config):
"""Run schema + final validation and return the validated config."""
return FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
def test_metadata_native_quad_default_test_card(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A quad-mode display with no explicit drawing gets a test card from final validation."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
config = validated_config({"model": "JC3636W518"})
get_instance(config)
meta = get_display_metadata(str(config["id"]))
assert meta is not None
assert meta.width == 360
assert meta.height == 360
# final validation auto-enables show_test_card when no drawing methods are configured
assert meta.has_writer is True
assert meta.has_hardware_rotation is True
def test_metadata_single_mode_with_dc_pin(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A single-mode display with no explicit drawing gets a test card from final validation."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = validated_config(
{
"model": "ST7735",
"dc_pin": 18,
}
)
get_instance(config)
meta = get_display_metadata(str(config["id"]))
assert meta is not None
assert meta.width == 128
assert meta.height == 160
assert meta.has_writer is True
assert meta.has_hardware_rotation is True
def test_metadata_custom_dimensions(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A custom model picks up explicit dimensions."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = validated_config(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {"width": 480, "height": 320},
"init_sequence": [[0xA0, 0x01]],
}
)
get_instance(config)
meta = get_display_metadata(str(config["id"]))
assert meta is not None
assert meta.width == 480
assert meta.height == 320
# final validation auto-enables show_test_card
assert meta.has_writer is True
assert meta.has_hardware_rotation is True
def test_metadata_with_test_card_has_writer(
set_core_config: SetCoreConfigCallable,
) -> None:
"""When show_test_card is enabled, has_writer should be True."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = validated_config(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {"width": 240, "height": 240},
"init_sequence": [[0xA0, 0x01]],
"show_test_card": True,
}
)
get_instance(config)
meta = get_display_metadata(str(config["id"]))
assert meta is not None
assert meta.has_writer is True
def test_metadata_no_swap_xy_not_full_hardware_rotation(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A model that disables swap_xy should report has_hardware_rotation=False."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
# JC3248W535 has swap_xy=cv.UNDEFINED -> transforms={mirror_x, mirror_y} only
config = validated_config({"model": "JC3248W535"})
get_instance(config)
meta = get_display_metadata(str(config["id"]))
assert meta is not None
assert meta.has_hardware_rotation is False
def test_metadata_multiple_displays_independent(
set_core_config: SetCoreConfigCallable,
) -> None:
"""Multiple displays each get their own metadata entry."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config_a = validated_config(
{
"id": "disp_a",
"model": "custom",
"dc_pin": 18,
"dimensions": {"width": 320, "height": 240},
"init_sequence": [[0xA0, 0x01]],
}
)
config_b = validated_config(
{
"id": "disp_b",
"model": "custom",
"dc_pin": 19,
"dimensions": {"width": 128, "height": 64},
"init_sequence": [[0xA0, 0x01]],
}
)
get_instance(config_a)
get_instance(config_b)
all_meta = get_all_display_metadata()
# final validation auto-enables show_test_card for both
assert all_meta["disp_a"] == DisplayMetaData(320, 240, True, True)
assert all_meta["disp_b"] == DisplayMetaData(128, 64, True, True)
def test_metadata_via_code_generation_native(
generate_main: Callable[[str | Path], str],
component_fixture_path: Callable[[str], Path],
) -> None:
"""Full code generation for native.yaml should produce correct metadata."""
generate_main(component_fixture_path("native.yaml"))
all_meta = get_all_display_metadata()
# native.yaml: model JC3636W518 -> 360x360, no writer, full hardware rotation
assert len(all_meta) == 1
meta = next(iter(all_meta.values()))
assert meta == DisplayMetaData(
width=360, height=360, has_writer=True, has_hardware_rotation=True
)
def test_metadata_via_code_generation_lvgl(
generate_main: Callable[[str | Path], str],
component_fixture_path: Callable[[str], Path],
) -> None:
"""Full code generation for lvgl.yaml should produce correct metadata."""
generate_main(component_fixture_path("lvgl.yaml"))
all_meta = get_all_display_metadata()
# lvgl.yaml: model ST7735 -> 128x160, no writer (lvgl draws directly), full hw rotation
assert len(all_meta) == 1
meta = next(iter(all_meta.values()))
assert meta == DisplayMetaData(
width=128, height=160, has_writer=False, has_hardware_rotation=True
)
+2 -7
View File
@@ -204,11 +204,6 @@ def test_transform_and_init_sequence_errors(
r"extra keys not allowed @ data\['brightness'\]",
id="brightness_not_supported",
),
pytest.param(
{"model": "T-DISPLAY-S3-PRO"},
"PSRAM is required for this display",
id="psram_required",
),
],
)
def test_esp32s3_specific_errors(
@@ -319,7 +314,7 @@ def test_native_generation(
main_cpp = generate_main(component_fixture_path("native.yaml"))
assert (
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1, 1>()"
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, 0, true, 1, 1>()"
in main_cpp
)
assert "set_init_sequence({240, 1, 8, 242" in main_cpp
@@ -335,7 +330,7 @@ def test_lvgl_generation(
main_cpp = generate_main(component_fixture_path("lvgl.yaml"))
assert (
"mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0>();"
"mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0, 0, true>();"
in main_cpp
)
assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp