mirror of
https://github.com/esphome/esphome.git
synced 2026-05-30 07:16:11 +08:00
[mipi_spi] Rotation and buffer size changes (#15047)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user