mirror of
https://github.com/esphome/esphome.git
synced 2026-06-01 01:19:45 +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_PANEL = "mdi:solar-panel"
|
||||||
ICON_SOLAR_POWER = "mdi:solar-power"
|
ICON_SOLAR_POWER = "mdi:solar-power"
|
||||||
|
|
||||||
|
KEY_METADATA = "metadata"
|
||||||
|
|
||||||
UNIT_AMPERE_HOUR = "Ah"
|
UNIT_AMPERE_HOUR = "Ah"
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from esphome import automation, core
|
from esphome import automation, core
|
||||||
from esphome.automation import maybe_simple_id
|
from esphome.automation import maybe_simple_id
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.const import KEY_METADATA
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_AUTO_CLEAR_ENABLED,
|
CONF_AUTO_CLEAR_ENABLED,
|
||||||
@@ -16,7 +19,9 @@ from esphome.const import (
|
|||||||
SCHEDULER_DONT_RUN,
|
SCHEDULER_DONT_RUN,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||||
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
|
DOMAIN = "display"
|
||||||
IS_PLATFORM_COMPONENT = True
|
IS_PLATFORM_COMPONENT = True
|
||||||
|
|
||||||
display_ns = cg.esphome_ns.namespace("display")
|
display_ns = cg.esphome_ns.namespace("display")
|
||||||
@@ -146,6 +151,39 @@ async def setup_display_core_(var, config):
|
|||||||
cg.add(var.show_test_card())
|
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):
|
async def register_display(var, config):
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await setup_display_core_(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); }
|
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
|
||||||
|
|
||||||
/// Internal method to set the display rotation with.
|
/// 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.
|
// Internal method to set display auto clearing.
|
||||||
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
|
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
|
from esphome import core, external_files
|
||||||
import esphome.codegen as cg
|
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
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_DEFAULTS,
|
CONF_DEFAULTS,
|
||||||
@@ -53,7 +53,6 @@ CONF_CHROMA_KEY = "chroma_key"
|
|||||||
CONF_ALPHA_CHANNEL = "alpha_channel"
|
CONF_ALPHA_CHANNEL = "alpha_channel"
|
||||||
CONF_INVERT_ALPHA = "invert_alpha"
|
CONF_INVERT_ALPHA = "invert_alpha"
|
||||||
CONF_IMAGES = "images"
|
CONF_IMAGES = "images"
|
||||||
KEY_METADATA = "metadata"
|
|
||||||
|
|
||||||
TRANSPARENCY_TYPES = (
|
TRANSPARENCY_TYPES = (
|
||||||
CONF_OPAQUE,
|
CONF_OPAQUE,
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left
|
|||||||
MADCTL_XFLIP = 0x02 # Mirror the display horizontally
|
MADCTL_XFLIP = 0x02 # Mirror the display horizontally
|
||||||
MADCTL_YFLIP = 0x01 # Mirror the display vertically
|
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
|
# Special constant for delays in command sequences
|
||||||
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
|
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_X in transforms
|
||||||
return CONF_SWAP_XY in transforms and CONF_MIRROR_Y 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:
|
if CONF_DIMENSIONS in config:
|
||||||
# Explicit dimensions, just use as is
|
# Explicit dimensions, just use as is
|
||||||
dimensions = config[CONF_DIMENSIONS]
|
dimensions = config[CONF_DIMENSIONS]
|
||||||
@@ -361,13 +369,12 @@ class DriverChip:
|
|||||||
)
|
)
|
||||||
offset_height = native_height - height - offset_height
|
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
|
# 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
|
width, height = height, width
|
||||||
offset_height, offset_width = offset_width, offset_height
|
offset_height, offset_width = offset_width, offset_height
|
||||||
return width, height, offset_width, offset_height
|
return width, height, offset_width, offset_height
|
||||||
|
|
||||||
def get_transform(self, config) -> dict[str, bool]:
|
def get_base_transform(self, config):
|
||||||
can_transform = self.rotation_as_transform(config)
|
|
||||||
transform = config.get(
|
transform = config.get(
|
||||||
CONF_TRANSFORM,
|
CONF_TRANSFORM,
|
||||||
{
|
{
|
||||||
@@ -376,14 +383,20 @@ class DriverChip:
|
|||||||
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
|
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if not isinstance(transform, dict):
|
if isinstance(transform, dict):
|
||||||
# Presumably disabled
|
return transform
|
||||||
|
|
||||||
|
# Transform is disabled
|
||||||
return {
|
return {
|
||||||
CONF_MIRROR_X: False,
|
CONF_MIRROR_X: False,
|
||||||
CONF_MIRROR_Y: False,
|
CONF_MIRROR_Y: False,
|
||||||
CONF_SWAP_XY: False,
|
CONF_SWAP_XY: False,
|
||||||
CONF_TRANSFORM: 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?
|
# Can we use the MADCTL register to set the rotation?
|
||||||
if can_transform and CONF_TRANSFORM not in config:
|
if can_transform and CONF_TRANSFORM not in config:
|
||||||
rotation = config[CONF_ROTATION]
|
rotation = config[CONF_ROTATION]
|
||||||
@@ -411,11 +424,15 @@ class DriverChip:
|
|||||||
return {cv.Required(CONF_SWAP_XY): cv.boolean}
|
return {cv.Required(CONF_SWAP_XY): cv.boolean}
|
||||||
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
|
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
|
||||||
|
|
||||||
def add_madctl(self, sequence: list, config: dict):
|
def get_madctl(self, transform: dict, config: dict) -> int:
|
||||||
# Add the MADCTL command to the sequence based on the configuration.
|
"""
|
||||||
use_flip = config.get(CONF_USE_AXIS_FLIPS)
|
Convert a transform to MADCTL bits
|
||||||
madctl = 0
|
:param transform: The transform dict
|
||||||
transform = self.get_transform(config)
|
: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]:
|
if transform[CONF_MIRROR_X]:
|
||||||
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
|
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
|
||||||
if transform[CONF_MIRROR_Y]:
|
if transform[CONF_MIRROR_Y]:
|
||||||
@@ -424,22 +441,28 @@ class DriverChip:
|
|||||||
madctl |= MADCTL_MV
|
madctl |= MADCTL_MV
|
||||||
if config[CONF_COLOR_ORDER] == MODE_BGR:
|
if config[CONF_COLOR_ORDER] == MODE_BGR:
|
||||||
madctl |= MADCTL_BGR
|
madctl |= MADCTL_BGR
|
||||||
sequence.append((MADCTL, madctl))
|
|
||||||
return 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):
|
def skip_command(self, command: str):
|
||||||
"""
|
"""
|
||||||
Allow suppressing a standard command in the init sequence.
|
Allow suppressing a standard command in the init sequence.
|
||||||
"""
|
"""
|
||||||
return self.get_default(f"no_{command.lower()}", False)
|
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.
|
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.
|
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
|
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.
|
MADCTL will be set if add_madctl is True
|
||||||
Returns a tuple of the init sequence and the computed MADCTL value.
|
Returns the init sequence
|
||||||
"""
|
"""
|
||||||
sequence = list(self.initsequence or ())
|
sequence = list(self.initsequence or ())
|
||||||
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
|
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
|
||||||
@@ -457,7 +480,8 @@ class DriverChip:
|
|||||||
|
|
||||||
if self.rotation_as_transform(config):
|
if self.rotation_as_transform(config):
|
||||||
LOGGER.info("Using hardware transform to implement rotation")
|
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]:
|
if config[CONF_INVERT_COLORS]:
|
||||||
sequence.append((INVON,))
|
sequence.append((INVON,))
|
||||||
else:
|
else:
|
||||||
@@ -471,7 +495,7 @@ class DriverChip:
|
|||||||
|
|
||||||
# Flatten the sequence into a list of bytes, with the length of each command
|
# Flatten the sequence into a list of bytes, with the length of each command
|
||||||
# or the delay flag inserted where needed
|
# or the delay flag inserted where needed
|
||||||
return flatten_sequence(sequence), madctl
|
return flatten_sequence(sequence)
|
||||||
|
|
||||||
|
|
||||||
def requires_buffer(config) -> bool:
|
def requires_buffer(config) -> bool:
|
||||||
|
|||||||
@@ -192,10 +192,9 @@ async def to_code(config):
|
|||||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||||
var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode)
|
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_model(config[CONF_MODEL]))
|
||||||
cg.add(var.set_init_sequence(sequence))
|
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_invert_colors(config[CONF_INVERT_COLORS]))
|
||||||
cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH]))
|
cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH]))
|
||||||
cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH]))
|
cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH]))
|
||||||
|
|||||||
@@ -392,9 +392,6 @@ void MIPI_DSI::dump_config() {
|
|||||||
"\n Model: %s"
|
"\n Model: %s"
|
||||||
"\n Width: %u"
|
"\n Width: %u"
|
||||||
"\n Height: %u"
|
"\n Height: %u"
|
||||||
"\n Mirror X: %s"
|
|
||||||
"\n Mirror Y: %s"
|
|
||||||
"\n Swap X/Y: %s"
|
|
||||||
"\n Rotation: %d degrees"
|
"\n Rotation: %d degrees"
|
||||||
"\n DSI Lanes: %u"
|
"\n DSI Lanes: %u"
|
||||||
"\n Lane Bit Rate: %.0fMbps"
|
"\n Lane Bit Rate: %.0fMbps"
|
||||||
@@ -406,14 +403,11 @@ void MIPI_DSI::dump_config() {
|
|||||||
"\n VSync Front Porch: %u"
|
"\n VSync Front Porch: %u"
|
||||||
"\n Buffer Color Depth: %d bit"
|
"\n Buffer Color Depth: %d bit"
|
||||||
"\n Display Pixel Mode: %d bit"
|
"\n Display Pixel Mode: %d bit"
|
||||||
"\n Color Order: %s"
|
|
||||||
"\n Invert Colors: %s"
|
"\n Invert Colors: %s"
|
||||||
"\n Pixel Clock: %.1fMHz",
|
"\n Pixel Clock: %.1fMHz",
|
||||||
this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)),
|
this->model_, this->width_, this->height_, this->rotation_, this->lanes_, this->lane_bit_rate_,
|
||||||
YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_,
|
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
|
||||||
this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_,
|
this->vsync_back_porch_, this->vsync_front_porch_, (3 - this->color_depth_) * 8, this->pixel_mode_,
|
||||||
this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, this->vsync_front_porch_,
|
|
||||||
(3 - this->color_depth_) * 8, this->pixel_mode_, this->madctl_ & MADCTL_BGR ? "BGR" : "RGB",
|
|
||||||
YESNO(this->invert_colors_), this->pclk_frequency_);
|
YESNO(this->invert_colors_), this->pclk_frequency_);
|
||||||
LOG_PIN(" Reset Pin ", this->reset_pin_);
|
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_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_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_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);
|
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_{};
|
std::vector<GPIOPin *> enable_pins_{};
|
||||||
size_t width_{};
|
size_t width_{};
|
||||||
size_t height_{};
|
size_t height_{};
|
||||||
uint8_t madctl_{};
|
|
||||||
uint16_t hsync_pulse_width_ = 10;
|
uint16_t hsync_pulse_width_ = 10;
|
||||||
uint16_t hsync_back_porch_ = 10;
|
uint16_t hsync_back_porch_ = 10;
|
||||||
uint16_t hsync_front_porch_ = 20;
|
uint16_t hsync_front_porch_ = 20;
|
||||||
|
|||||||
@@ -265,9 +265,8 @@ async def to_code(config):
|
|||||||
|
|
||||||
if CONF_SPI_ID in config:
|
if CONF_SPI_ID in config:
|
||||||
await spi.register_spi_device(var, config, write_only=True)
|
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_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_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
|
||||||
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
|
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
|
||||||
|
|||||||
@@ -118,15 +118,7 @@ void MipiRgbSpi::dump_config() {
|
|||||||
MipiRgb::dump_config();
|
MipiRgb::dump_config();
|
||||||
LOG_PIN(" CS Pin: ", this->cs_);
|
LOG_PIN(" CS Pin: ", this->cs_);
|
||||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG, " SPI Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000));
|
||||||
" 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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // USE_SPI
|
#endif // USE_SPI
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class MipiRgb : public display::Display {
|
|||||||
display::ColorOrder get_color_mode() { return this->color_mode_; }
|
display::ColorOrder get_color_mode() { return this->color_mode_; }
|
||||||
void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; }
|
void set_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_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 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; }
|
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;
|
uint16_t vsync_front_porch_ = 10;
|
||||||
uint32_t pclk_frequency_ = 16 * 1000 * 1000;
|
uint32_t pclk_frequency_ = 16 * 1000 * 1000;
|
||||||
bool pclk_inverted_{true};
|
bool pclk_inverted_{true};
|
||||||
uint8_t madctl_{};
|
|
||||||
const char *model_{"Unknown"};
|
const char *model_{"Unknown"};
|
||||||
bool invert_colors_{};
|
bool invert_colors_{};
|
||||||
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
|
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from esphome.components.const import (
|
|||||||
CONF_COLOR_DEPTH,
|
CONF_COLOR_DEPTH,
|
||||||
CONF_DRAW_ROUNDING,
|
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 (
|
from esphome.components.mipi import (
|
||||||
CONF_PIXEL_MODE,
|
CONF_PIXEL_MODE,
|
||||||
CONF_USE_AXIS_FLIPS,
|
CONF_USE_AXIS_FLIPS,
|
||||||
@@ -47,12 +47,10 @@ from esphome.const import (
|
|||||||
CONF_MIRROR_Y,
|
CONF_MIRROR_Y,
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
CONF_RESET_PIN,
|
CONF_RESET_PIN,
|
||||||
CONF_ROTATION,
|
|
||||||
CONF_SWAP_XY,
|
CONF_SWAP_XY,
|
||||||
CONF_TRANSFORM,
|
CONF_TRANSFORM,
|
||||||
CONF_WIDTH,
|
CONF_WIDTH,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
|
||||||
from esphome.cpp_generator import TemplateArguments
|
from esphome.cpp_generator import TemplateArguments
|
||||||
from esphome.final_validate import full_config
|
from esphome.final_validate import full_config
|
||||||
|
|
||||||
@@ -113,22 +111,21 @@ DISPLAY_PIXEL_MODES = {
|
|||||||
def denominator(config):
|
def denominator(config):
|
||||||
"""
|
"""
|
||||||
Calculate the best denominator for a buffer size fraction.
|
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.
|
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
|
:config: The configuration dictionary containing the buffer size fraction and display dimensions
|
||||||
:return: The denominator to use for the buffer size fraction
|
:return: The denominator to use for the buffer size fraction
|
||||||
"""
|
"""
|
||||||
model = MODELS[config[CONF_MODEL]]
|
model = MODELS[config[CONF_MODEL]]
|
||||||
frac = config.get(CONF_BUFFER_SIZE)
|
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
|
return 1
|
||||||
height, _width, _offset_width, _offset_height = model.get_dimensions(config)
|
|
||||||
try:
|
try:
|
||||||
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
|
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise cv.Invalid(
|
# No exact divisor, just use the closest.
|
||||||
f"Buffer size fraction {frac} is not compatible with display height {height}"
|
return next(x for x in range(2, 17) if frac >= 1 / x)
|
||||||
) from StopIteration
|
|
||||||
|
|
||||||
|
|
||||||
def model_schema(config):
|
def model_schema(config):
|
||||||
@@ -287,30 +284,19 @@ def _final_validate(config):
|
|||||||
config[CONF_SHOW_TEST_CARD] = True
|
config[CONF_SHOW_TEST_CARD] = True
|
||||||
|
|
||||||
if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
|
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 PSRAM is not enabled, choose a small buffer size by default
|
||||||
if not requires_buffer(config):
|
if not requires_buffer(config):
|
||||||
# not our problem.
|
return config # No buffer needed, so no need to set a buffer size
|
||||||
return config
|
|
||||||
color_depth = get_color_depth(config)
|
color_depth = get_color_depth(config)
|
||||||
frac = denominator(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
|
buffer_size = color_depth // 8 * width * height // frac
|
||||||
# Target a buffer size of 20kB
|
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
|
||||||
fraction = 20000.0 / buffer_size
|
fraction = min(20000.0, buffer_size // 16) / buffer_size
|
||||||
try:
|
|
||||||
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
||||||
x for x in range(2, 17) if fraction >= 1 / x and height % x == 0
|
x for x in range(2, 17) if fraction >= 1 / x
|
||||||
)
|
)
|
||||||
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
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@@ -318,39 +304,6 @@ def _final_validate(config):
|
|||||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
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):
|
def get_instance(config):
|
||||||
"""
|
"""
|
||||||
Get the type of MipiSpi instance to create based on the configuration,
|
Get the type of MipiSpi instance to create based on the configuration,
|
||||||
@@ -359,7 +312,16 @@ def get_instance(config):
|
|||||||
:return: type, template arguments
|
:return: type, template arguments
|
||||||
"""
|
"""
|
||||||
model = MODELS[config[CONF_MODEL]]
|
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"))
|
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
|
||||||
bufferpixels = COLOR_DEPTHS[color_depth]
|
bufferpixels = COLOR_DEPTHS[color_depth]
|
||||||
@@ -373,57 +335,43 @@ def get_instance(config):
|
|||||||
bus_type = BusTypes[bus_type]
|
bus_type = BusTypes[bus_type]
|
||||||
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
|
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
|
||||||
frac = denominator(config)
|
frac = denominator(config)
|
||||||
rotation = (
|
madctl = model.get_madctl(model.get_base_transform(config), config)
|
||||||
0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0)
|
has_writer = requires_buffer(config)
|
||||||
)
|
|
||||||
templateargs = [
|
templateargs = [
|
||||||
buffer_type,
|
buffer_type,
|
||||||
bufferpixels,
|
bufferpixels,
|
||||||
config[CONF_BYTE_ORDER] == "big_endian",
|
config[CONF_BYTE_ORDER] == "big_endian",
|
||||||
display_pixel_mode,
|
display_pixel_mode,
|
||||||
bus_type,
|
bus_type,
|
||||||
]
|
|
||||||
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
|
|
||||||
if requires_buffer(config):
|
|
||||||
templateargs.extend(
|
|
||||||
[
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
offset_width,
|
offset_width,
|
||||||
offset_height,
|
offset_height,
|
||||||
DISPLAY_ROTATIONS[rotation],
|
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(
|
||||||
|
[
|
||||||
frac,
|
frac,
|
||||||
config[CONF_DRAW_ROUNDING],
|
config[CONF_DRAW_ROUNDING],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return MipiSpiBuffer, templateargs
|
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
|
return MipiSpi, templateargs
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
model = MODELS[config[CONF_MODEL]]
|
model = MODELS[config[CONF_MODEL]]
|
||||||
var_id = config[CONF_ID]
|
var_id = config[CONF_ID]
|
||||||
|
init_sequence = model.get_sequence(config, False)
|
||||||
var_id.type, templateargs = get_instance(config)
|
var_id.type, templateargs = get_instance(config)
|
||||||
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
|
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
|
||||||
init_sequence, _madctl = model.get_sequence(config)
|
|
||||||
cg.add(var.set_init_sequence(init_sequence))
|
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]))
|
cg.add(var.set_model(config[CONF_MODEL]))
|
||||||
if enable_pin := config.get(CONF_ENABLE_PIN):
|
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||||
enable = [await cg.gpio_pin_expression(pin) for pin in 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,
|
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,
|
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,
|
ESP_LOGCONFIG(TAG,
|
||||||
"MIPI_SPI Display\n"
|
"MIPI_SPI Display\n"
|
||||||
" Model: %s\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"
|
" Swap X/Y: %s\n"
|
||||||
" Mirror X: %s\n"
|
" Mirror X: %s\n"
|
||||||
" Mirror Y: %s\n"
|
" Mirror Y: %s\n"
|
||||||
|
" Hardware rotation: %s\n"
|
||||||
" Invert colors: %s\n"
|
" Invert colors: %s\n"
|
||||||
" Color order: %s\n"
|
" Color order: %s\n"
|
||||||
" Display pixels: %d bits\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 Data rate: %uMHz\n"
|
||||||
" SPI Bus width: %d",
|
" SPI Bus width: %d",
|
||||||
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
|
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",
|
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(has_hardware_rotation), YESNO(invert_colors),
|
||||||
display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast<unsigned>(data_rate / 1000000),
|
(madctl & MADCTL_BGR) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode,
|
||||||
bus_width);
|
static_cast<unsigned>(data_rate / 1000000), bus_width);
|
||||||
LOG_PIN(" CS Pin: ", cs);
|
LOG_PIN(" CS Pin: ", cs);
|
||||||
LOG_PIN(" Reset Pin: ", reset);
|
LOG_PIN(" Reset Pin: ", reset);
|
||||||
LOG_PIN(" DC Pin: ", dc);
|
LOG_PIN(" DC Pin: ", dc);
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel ord
|
|||||||
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red 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_XFLIP = 0x02; // Mirror the display horizontally
|
||||||
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
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;
|
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||||
// store a 16 bit value in a buffer, big endian.
|
// 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
|
// 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,
|
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,
|
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.
|
* 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
|
* buffer
|
||||||
*/
|
*/
|
||||||
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
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,
|
class MipiSpi : public display::Display,
|
||||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||||
spi::DATA_RATE_1MHZ> {
|
spi::DATA_RATE_1MHZ> {
|
||||||
@@ -103,10 +105,39 @@ class MipiSpi : public display::Display,
|
|||||||
this->brightness_ = brightness;
|
this->brightness_ = brightness;
|
||||||
this->reset_params_();
|
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; }
|
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||||
|
|
||||||
int get_width_internal() override { return WIDTH; }
|
int get_width() override {
|
||||||
int get_height_internal() override { return HEIGHT; }
|
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; }
|
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
||||||
|
|
||||||
// reset the display, and write the init sequence
|
// reset the display, and write the init sequence
|
||||||
@@ -166,9 +197,6 @@ class MipiSpi : public display::Display,
|
|||||||
case INVERT_ON:
|
case INVERT_ON:
|
||||||
this->invert_colors_ = true;
|
this->invert_colors_ = true;
|
||||||
break;
|
break;
|
||||||
case MADCTL_CMD:
|
|
||||||
this->madctl_ = arg_byte;
|
|
||||||
break;
|
|
||||||
case BRIGHTNESS:
|
case BRIGHTNESS:
|
||||||
this->brightness_ = arg_byte;
|
this->brightness_ = arg_byte;
|
||||||
break;
|
break;
|
||||||
@@ -177,13 +205,13 @@ class MipiSpi : public display::Display,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const auto *ptr = vec.data() + index;
|
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);
|
this->write_command_(cmd, ptr, num_args);
|
||||||
index += num_args;
|
index += num_args;
|
||||||
if (cmd == SLEEP_OUT)
|
if (cmd == SLEEP_OUT)
|
||||||
delay(10);
|
delay(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this->reset_params_();
|
||||||
// init sequence no longer needed
|
// init sequence no longer needed
|
||||||
this->init_sequence_.clear();
|
this->init_sequence_.clear();
|
||||||
}
|
}
|
||||||
@@ -206,9 +234,10 @@ class MipiSpi : public display::Display,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dump_config() override {
|
void dump_config() override {
|
||||||
internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_,
|
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
|
||||||
DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_,
|
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
|
||||||
this->mode_, this->data_rate_, BUS_TYPE);
|
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||||
|
HAS_HARDWARE_ROTATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -219,10 +248,13 @@ class MipiSpi : public display::Display,
|
|||||||
|
|
||||||
// Writes a command to the display, with the given bytes.
|
// Writes a command to the display, with the given bytes.
|
||||||
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
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)];
|
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
|
||||||
|
// 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));
|
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||||
#endif
|
} 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) {
|
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||||
this->enable();
|
this->enable();
|
||||||
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
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);
|
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
||||||
if (this->brightness_.has_value())
|
if (this->brightness_.has_value())
|
||||||
this->write_command_(BRIGHTNESS, this->brightness_.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
|
// 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) {
|
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);
|
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
|
||||||
uint8_t buf[4];
|
uint8_t buf[4];
|
||||||
x1 += OFFSET_WIDTH;
|
x1 += get_offset_width_();
|
||||||
x2 += OFFSET_WIDTH;
|
x2 += get_offset_width_();
|
||||||
y1 += OFFSET_HEIGHT;
|
y1 += get_offset_height_();
|
||||||
y2 += OFFSET_HEIGHT;
|
y2 += get_offset_height_();
|
||||||
put16_be(buf, y1);
|
put16_be(buf, y1);
|
||||||
put16_be(buf + 2, y2);
|
put16_be(buf + 2, y2);
|
||||||
this->write_command_(RASET, buf, sizeof buf);
|
this->write_command_(RASET, buf, sizeof buf);
|
||||||
@@ -408,7 +484,6 @@ class MipiSpi : public display::Display,
|
|||||||
optional<uint8_t> brightness_{};
|
optional<uint8_t> brightness_{};
|
||||||
const char *model_{"Unknown"};
|
const char *model_{"Unknown"};
|
||||||
std::vector<uint8_t> init_sequence_{};
|
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)
|
* @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,
|
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,
|
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL,
|
||||||
int FRACTION, unsigned ROUNDING>
|
bool HAS_HARDWARE_ROTATION, int FRACTION, unsigned ROUNDING>
|
||||||
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
|
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:
|
public:
|
||||||
// these values define the buffer size needed to write in accordance with the chip pixel alignment
|
// 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
|
// 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.
|
// 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 size_t round_buffer(size_t size) { return (size + ROUNDING - 1) / ROUNDING * ROUNDING; }
|
||||||
static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING;
|
|
||||||
|
|
||||||
MipiSpiBuffer() { this->rotation_ = ROTATION; }
|
MipiSpiBuffer() = default;
|
||||||
|
|
||||||
void dump_config() override {
|
void dump_config() override {
|
||||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||||
OFFSET_HEIGHT>::dump_config();
|
MADCTL, HAS_HARDWARE_ROTATION>::dump_config();
|
||||||
esph_log_config(TAG,
|
esph_log_config(TAG,
|
||||||
" Rotation: %d°\n"
|
" Rotation: %d°\n"
|
||||||
" Buffer pixels: %d bits\n"
|
" Buffer pixels: %d bits\n"
|
||||||
@@ -450,14 +524,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
" Buffer bytes: %zu\n"
|
" Buffer bytes: %zu\n"
|
||||||
" Draw rounding: %u",
|
" Draw rounding: %u",
|
||||||
this->rotation_, BUFFERPIXEL * 8, FRACTION,
|
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 {
|
void setup() override {
|
||||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||||
OFFSET_HEIGHT>::setup();
|
MADCTL, HAS_HARDWARE_ROTATION>::setup();
|
||||||
RAMAllocator<BUFFERTYPE> allocator{};
|
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) {
|
if (this->buffer_ == nullptr) {
|
||||||
this->mark_failed(LOG_STR("Buffer allocation failed"));
|
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
|
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
|
||||||
// the display height,
|
// 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
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
auto lap = millis();
|
auto lap = millis();
|
||||||
#endif
|
#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_) {
|
if (this->auto_clear_enabled_) {
|
||||||
this->clear();
|
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 w = this->x_high_ - this->x_low_ + 1;
|
||||||
int h = this->y_high_ - this->y_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->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
|
// invalidate watermarks
|
||||||
this->x_low_ = WIDTH;
|
this->x_low_ = this->get_width_internal();
|
||||||
this->y_low_ = HEIGHT;
|
this->y_low_ = this->get_height_internal();
|
||||||
this->x_high_ = 0;
|
this->x_high_ = 0;
|
||||||
this->y_high_ = 0;
|
this->y_high_ = 0;
|
||||||
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
#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 {
|
void draw_pixel_at(int x, int y, Color color) override {
|
||||||
if (!this->get_clipping().inside(x, y))
|
if (!this->get_clipping().inside(x, y))
|
||||||
return;
|
return;
|
||||||
rotate_coordinates(x, y);
|
if constexpr (not HAS_HARDWARE_ROTATION) {
|
||||||
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
|
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;
|
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_) {
|
if (x < this->x_low_) {
|
||||||
this->x_low_ = x;
|
this->x_low_ = x;
|
||||||
}
|
}
|
||||||
@@ -551,39 +640,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
|
|
||||||
this->x_low_ = 0;
|
this->x_low_ = 0;
|
||||||
this->y_low_ = this->start_line_;
|
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;
|
this->y_high_ = this->end_line_ - 1;
|
||||||
std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color));
|
std::fill_n(this->buffer_, (this->end_line_ - this->start_line_) * round_buffer(this->get_width_internal()),
|
||||||
}
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Rotate the coordinates to match the display orientation.
|
// 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.
|
// Convert a color to the buffer pixel format.
|
||||||
static BUFFERTYPE convert_color(const Color &color) {
|
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'\]",
|
r"extra keys not allowed @ data\['brightness'\]",
|
||||||
id="brightness_not_supported",
|
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(
|
def test_esp32s3_specific_errors(
|
||||||
@@ -319,7 +314,7 @@ def test_native_generation(
|
|||||||
|
|
||||||
main_cpp = generate_main(component_fixture_path("native.yaml"))
|
main_cpp = generate_main(component_fixture_path("native.yaml"))
|
||||||
assert (
|
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
|
in main_cpp
|
||||||
)
|
)
|
||||||
assert "set_init_sequence({240, 1, 8, 242" 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"))
|
main_cpp = generate_main(component_fixture_path("lvgl.yaml"))
|
||||||
assert (
|
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
|
in main_cpp
|
||||||
)
|
)
|
||||||
assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp
|
assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp
|
||||||
|
|||||||
Reference in New Issue
Block a user