mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 13:27:14 +00:00
[mipi_spi] Implement automatic mapping of offsets (#16722)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
@@ -139,6 +139,8 @@ 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
|
||||||
|
|
||||||
|
CONF_PAD_HEIGHT = "pad_height"
|
||||||
|
CONF_PAD_WIDTH = "pad_width"
|
||||||
CONF_PIXEL_MODE = "pixel_mode"
|
CONF_PIXEL_MODE = "pixel_mode"
|
||||||
CONF_USE_AXIS_FLIPS = "use_axis_flips"
|
CONF_USE_AXIS_FLIPS = "use_axis_flips"
|
||||||
|
|
||||||
@@ -202,6 +204,8 @@ def dimension_schema(rounding):
|
|||||||
rounding
|
rounding
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding),
|
cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding),
|
||||||
|
cv.Optional(CONF_PAD_WIDTH): validate_dimension(rounding),
|
||||||
|
cv.Optional(CONF_PAD_HEIGHT): validate_dimension(rounding),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -311,6 +315,36 @@ class DriverChip:
|
|||||||
name = name.upper()
|
name = name.upper()
|
||||||
self.name = name
|
self.name = name
|
||||||
self.initsequence = initsequence
|
self.initsequence = initsequence
|
||||||
|
if CONF_NATIVE_WIDTH in defaults:
|
||||||
|
if CONF_WIDTH not in defaults:
|
||||||
|
defaults[CONF_WIDTH] = (
|
||||||
|
defaults[CONF_NATIVE_WIDTH]
|
||||||
|
- defaults.get(CONF_OFFSET_WIDTH, 0)
|
||||||
|
- defaults.get(CONF_PAD_WIDTH, 0)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
native_width = (
|
||||||
|
defaults.get(CONF_WIDTH, 0)
|
||||||
|
+ defaults.get(CONF_OFFSET_WIDTH, 0)
|
||||||
|
+ defaults.get(CONF_PAD_WIDTH, 0)
|
||||||
|
)
|
||||||
|
if native_width != 0:
|
||||||
|
defaults[CONF_NATIVE_WIDTH] = native_width
|
||||||
|
if CONF_NATIVE_HEIGHT in defaults:
|
||||||
|
if CONF_HEIGHT not in defaults:
|
||||||
|
defaults[CONF_HEIGHT] = (
|
||||||
|
defaults[CONF_NATIVE_HEIGHT]
|
||||||
|
- defaults.get(CONF_OFFSET_HEIGHT, 0)
|
||||||
|
- defaults.get(CONF_PAD_HEIGHT, 0)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
native_height = (
|
||||||
|
defaults.get(CONF_HEIGHT, 0)
|
||||||
|
+ defaults.get(CONF_OFFSET_HEIGHT, 0)
|
||||||
|
+ defaults.get(CONF_PAD_HEIGHT, 0)
|
||||||
|
)
|
||||||
|
if native_height != 0:
|
||||||
|
defaults[CONF_NATIVE_HEIGHT] = native_height
|
||||||
self.defaults = defaults
|
self.defaults = defaults
|
||||||
DriverChip.models[name] = self
|
DriverChip.models[name] = self
|
||||||
|
|
||||||
@@ -336,18 +370,6 @@ class DriverChip:
|
|||||||
initsequence = list(kwargs.pop("initsequence", self.initsequence))
|
initsequence = list(kwargs.pop("initsequence", self.initsequence))
|
||||||
initsequence.extend(kwargs.pop("add_init_sequence", ()))
|
initsequence.extend(kwargs.pop("add_init_sequence", ()))
|
||||||
defaults = self.defaults.copy()
|
defaults = self.defaults.copy()
|
||||||
if (
|
|
||||||
CONF_WIDTH in defaults
|
|
||||||
and CONF_OFFSET_WIDTH in kwargs
|
|
||||||
and CONF_NATIVE_WIDTH not in defaults
|
|
||||||
):
|
|
||||||
defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH]
|
|
||||||
if (
|
|
||||||
CONF_HEIGHT in defaults
|
|
||||||
and CONF_OFFSET_HEIGHT in kwargs
|
|
||||||
and CONF_NATIVE_HEIGHT not in defaults
|
|
||||||
):
|
|
||||||
defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT]
|
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
return self.__class__(name, initsequence=tuple(initsequence), **defaults)
|
return self.__class__(name, initsequence=tuple(initsequence), **defaults)
|
||||||
|
|
||||||
@@ -385,13 +407,16 @@ 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, swap: bool = True) -> tuple[int, int, int, int]:
|
def get_dimensions(
|
||||||
|
self, config, swap: bool = True
|
||||||
|
) -> tuple[int, int, int, int, int, int]:
|
||||||
"""
|
"""
|
||||||
Return the dimensions of the current model.
|
Return the dimensions of the current model.
|
||||||
:param config: The current configuration
|
:param config: The current configuration
|
||||||
:param swap: If width/height should be swapped when axes are swapped.
|
:param swap: If width/height should be swapped when axes are swapped.
|
||||||
:return:
|
:return: A tuple (width, height, offset_width, offset_height, pad_width, pad_height).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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]
|
||||||
@@ -400,33 +425,71 @@ class DriverChip:
|
|||||||
height = dimensions[CONF_HEIGHT]
|
height = dimensions[CONF_HEIGHT]
|
||||||
offset_width = dimensions[CONF_OFFSET_WIDTH]
|
offset_width = dimensions[CONF_OFFSET_WIDTH]
|
||||||
offset_height = dimensions[CONF_OFFSET_HEIGHT]
|
offset_height = dimensions[CONF_OFFSET_HEIGHT]
|
||||||
return width, height, offset_width, offset_height
|
if CONF_PAD_WIDTH in dimensions:
|
||||||
(width, height) = dimensions
|
pad_width = dimensions[CONF_PAD_WIDTH]
|
||||||
return width, height, 0, 0
|
native_width = width + offset_width + pad_width
|
||||||
|
else:
|
||||||
|
native_width = self.get_default(CONF_NATIVE_WIDTH, 0)
|
||||||
|
if native_width == 0:
|
||||||
|
pad_width = 0
|
||||||
|
native_width = width + offset_width
|
||||||
|
else:
|
||||||
|
pad_width = native_width - width - offset_width
|
||||||
|
if CONF_PAD_HEIGHT in dimensions:
|
||||||
|
pad_height = dimensions[CONF_PAD_HEIGHT]
|
||||||
|
native_height = height + offset_height + pad_height
|
||||||
|
else:
|
||||||
|
native_height = self.get_default(CONF_NATIVE_HEIGHT, 0)
|
||||||
|
if native_height == 0:
|
||||||
|
pad_height = 0
|
||||||
|
native_height = height + offset_height
|
||||||
|
else:
|
||||||
|
pad_height = native_height - height - offset_height
|
||||||
|
if (
|
||||||
|
pad_width + offset_width >= native_width
|
||||||
|
or pad_height + offset_height >= native_height
|
||||||
|
):
|
||||||
|
raise cv.Invalid("Dimensions exceed native size", [CONF_DIMENSIONS])
|
||||||
|
if pad_width < 0 or pad_height < 0:
|
||||||
|
raise cv.Invalid("Invalid offsets", [CONF_DIMENSIONS])
|
||||||
|
|
||||||
|
return width, height, offset_width, offset_height, pad_width, pad_height
|
||||||
|
|
||||||
|
# Must be a tuple
|
||||||
|
width, height = dimensions
|
||||||
|
return width, height, 0, 0, 0, 0
|
||||||
|
|
||||||
# Default dimensions, use model defaults
|
# Default dimensions, use model defaults
|
||||||
transform = self.get_transform(config)
|
transform = self.get_transform(config)
|
||||||
|
|
||||||
width = self.get_default(CONF_WIDTH)
|
width = self.get_default(CONF_WIDTH)
|
||||||
height = self.get_default(CONF_HEIGHT)
|
height = self.get_default(CONF_HEIGHT)
|
||||||
|
native_width = self.get_default(CONF_NATIVE_WIDTH, 0)
|
||||||
|
native_height = self.get_default(CONF_NATIVE_HEIGHT, 0)
|
||||||
offset_width = self.get_default(CONF_OFFSET_WIDTH, 0)
|
offset_width = self.get_default(CONF_OFFSET_WIDTH, 0)
|
||||||
offset_height = self.get_default(CONF_OFFSET_HEIGHT, 0)
|
offset_height = self.get_default(CONF_OFFSET_HEIGHT, 0)
|
||||||
|
pad_width = self.get_default(
|
||||||
|
CONF_PAD_WIDTH, native_width - width - offset_width
|
||||||
|
)
|
||||||
|
pad_height = self.get_default(
|
||||||
|
CONF_PAD_HEIGHT, native_height - height - offset_height
|
||||||
|
)
|
||||||
|
|
||||||
|
if pad_width < 0 or pad_height < 0:
|
||||||
|
raise cv.Invalid("Offsets exceed native size", [CONF_DIMENSIONS])
|
||||||
|
|
||||||
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
|
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
|
||||||
# the offset is asymmetric
|
# the offset is asymmetric
|
||||||
if transform.get(CONF_MIRROR_X):
|
if transform.get(CONF_MIRROR_X):
|
||||||
native_width = self.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2)
|
offset_width, pad_width = pad_width, offset_width
|
||||||
offset_width = native_width - width - offset_width
|
|
||||||
if transform.get(CONF_MIRROR_Y):
|
if transform.get(CONF_MIRROR_Y):
|
||||||
native_height = self.get_default(
|
offset_height, pad_height = pad_height, offset_height
|
||||||
CONF_NATIVE_HEIGHT, height + offset_height * 2
|
# Swap default dimensions if swap_xy is set, or if rotation is 90/270, and we are not using a buffer
|
||||||
)
|
|
||||||
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 swap and 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
|
pad_width, pad_height = pad_height, pad_width
|
||||||
|
return width, height, offset_width, offset_height, pad_width, pad_height
|
||||||
|
|
||||||
def get_base_transform(self, config):
|
def get_base_transform(self, config):
|
||||||
transform = config.get(
|
transform = config.get(
|
||||||
@@ -450,20 +513,8 @@ class DriverChip:
|
|||||||
|
|
||||||
def get_transform(self, config) -> dict[str, bool]:
|
def get_transform(self, config) -> dict[str, bool]:
|
||||||
transform = self.get_base_transform(config)
|
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:
|
transform[CONF_TRANSFORM] = self.rotation_as_transform(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
|
return transform
|
||||||
|
|
||||||
def swap_xy_schema(self):
|
def swap_xy_schema(self):
|
||||||
@@ -498,8 +549,8 @@ class DriverChip:
|
|||||||
return madctl
|
return madctl
|
||||||
|
|
||||||
def add_madctl(self, sequence: list, config: dict):
|
def add_madctl(self, sequence: list, config: dict):
|
||||||
# Add the MADCTL command to the sequence based on the configuration.
|
# Add the MADCTL command to the sequence based on the base configuration.
|
||||||
# This takes into account rotation if it can be implemented in the transform
|
# Rotation is not applied here, it will be done at runtime.
|
||||||
transform = self.get_transform(config)
|
transform = self.get_transform(config)
|
||||||
madctl = self.get_madctl(transform, config)
|
madctl = self.get_madctl(transform, config)
|
||||||
sequence.append((MADCTL, madctl & 0xFF))
|
sequence.append((MADCTL, madctl & 0xFF))
|
||||||
|
|||||||
@@ -172,7 +172,9 @@ def _config_schema(config):
|
|||||||
)(config)
|
)(config)
|
||||||
config = model_schema(config)(config)
|
config = model_schema(config)(config)
|
||||||
model = MODELS[config[CONF_MODEL].upper()]
|
model = MODELS[config[CONF_MODEL].upper()]
|
||||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||||
|
model.get_dimensions(config)
|
||||||
|
)
|
||||||
display.add_metadata(
|
display.add_metadata(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
width,
|
width,
|
||||||
@@ -206,7 +208,9 @@ async def to_code(config):
|
|||||||
model = MODELS[config[CONF_MODEL].upper()]
|
model = MODELS[config[CONF_MODEL].upper()]
|
||||||
color_depth = COLOR_DEPTHS[get_color_depth(config)]
|
color_depth = COLOR_DEPTHS[get_color_depth(config)]
|
||||||
pixel_mode = int(config[CONF_PIXEL_MODE].removesuffix("bit"))
|
pixel_mode = int(config[CONF_PIXEL_MODE].removesuffix("bit"))
|
||||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
width, height, _offset_width, _offset_height, _pad_width, _pad_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 = model.get_sequence(config)
|
sequence = model.get_sequence(config)
|
||||||
|
|||||||
@@ -235,7 +235,9 @@ def _config_schema(config):
|
|||||||
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
|
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
|
||||||
)(config)
|
)(config)
|
||||||
model = MODELS[config[CONF_MODEL].upper()]
|
model = MODELS[config[CONF_MODEL].upper()]
|
||||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||||
|
model.get_dimensions(config)
|
||||||
|
)
|
||||||
display.add_metadata(
|
display.add_metadata(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
width,
|
width,
|
||||||
@@ -273,7 +275,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
|||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
model = MODELS[config[CONF_MODEL].upper()]
|
model = MODELS[config[CONF_MODEL].upper()]
|
||||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||||
|
model.get_dimensions(config)
|
||||||
|
)
|
||||||
var = cg.new_Pvariable(config[CONF_ID], width, height)
|
var = cg.new_Pvariable(config[CONF_ID], width, height)
|
||||||
cg.add(var.set_model(model.name))
|
cg.add(var.set_model(model.name))
|
||||||
if enable_pin := config.get(CONF_ENABLE_PIN):
|
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from esphome.components.mipi import (
|
|||||||
requires_buffer,
|
requires_buffer,
|
||||||
)
|
)
|
||||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||||
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
from esphome.components.spi import CONF_SPI_MODE, TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.config_validation import ALLOW_EXTRA
|
from esphome.config_validation import ALLOW_EXTRA
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -121,7 +121,9 @@ def denominator(config):
|
|||||||
"""
|
"""
|
||||||
model = MODELS[config[CONF_MODEL]]
|
model = MODELS[config[CONF_MODEL]]
|
||||||
frac = config.get(CONF_BUFFER_SIZE)
|
frac = config.get(CONF_BUFFER_SIZE)
|
||||||
_width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
_width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||||
|
model.get_dimensions(config)
|
||||||
|
)
|
||||||
if frac is None or frac > 0.75 or height < 32:
|
if frac is None or frac > 0.75 or height < 32:
|
||||||
return 1
|
return 1
|
||||||
try:
|
try:
|
||||||
@@ -169,11 +171,22 @@ def model_schema(config):
|
|||||||
]
|
]
|
||||||
if bus_mode == TYPE_SINGLE:
|
if bus_mode == TYPE_SINGLE:
|
||||||
other_options.append(CONF_SPI_16)
|
other_options.append(CONF_SPI_16)
|
||||||
|
# Calculate default SPI mode. Mode3 for octal bus or single bus with no cs pin, mode0 otherwise.
|
||||||
|
spi_mode = model.get_default(CONF_SPI_MODE)
|
||||||
|
if not spi_mode:
|
||||||
|
if bus_mode == TYPE_OCTAL or (
|
||||||
|
bus_mode == TYPE_SINGLE
|
||||||
|
and not config.get(CONF_CS_PIN, model.get_default(CONF_CS_PIN))
|
||||||
|
):
|
||||||
|
spi_mode = "MODE3"
|
||||||
|
else:
|
||||||
|
spi_mode = "MODE0"
|
||||||
|
|
||||||
schema = (
|
schema = (
|
||||||
display.FULL_DISPLAY_SCHEMA.extend(
|
display.FULL_DISPLAY_SCHEMA.extend(
|
||||||
spi.spi_device_schema(
|
spi.spi_device_schema(
|
||||||
cs_pin_required=False,
|
cs_pin_required=False,
|
||||||
default_mode="MODE3" if bus_mode == TYPE_OCTAL else "MODE0",
|
default_mode=spi_mode,
|
||||||
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
|
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
|
||||||
mode=bus_mode,
|
mode=bus_mode,
|
||||||
)
|
)
|
||||||
@@ -279,8 +292,8 @@ def customise_schema(config):
|
|||||||
CONF_MIRROR_Y,
|
CONF_MIRROR_Y,
|
||||||
CONF_SWAP_XY,
|
CONF_SWAP_XY,
|
||||||
}
|
}
|
||||||
width, height, _offset_width, _offset_height = model.get_dimensions(
|
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||||
config, not has_hardware_transform
|
model.get_dimensions(config, not has_hardware_transform)
|
||||||
)
|
)
|
||||||
display.add_metadata(
|
display.add_metadata(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
@@ -313,14 +326,17 @@ def _final_validate(config):
|
|||||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||||
config[CONF_SHOW_TEST_CARD] = True
|
config[CONF_SHOW_TEST_CARD] = True
|
||||||
|
|
||||||
|
# Always call this to check dimensions during validation
|
||||||
|
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||||
|
model.get_dimensions(config)
|
||||||
|
)
|
||||||
|
|
||||||
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 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):
|
||||||
return # No need to pick a size
|
return # No need to pick a size
|
||||||
color_depth = get_color_depth(config)
|
color_depth = get_color_depth(config)
|
||||||
frac = denominator(config)
|
frac = denominator(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, except for large displays, which shouldn't end up here
|
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
|
||||||
fraction = min(20000.0, buffer_size // 4) / buffer_size
|
fraction = min(20000.0, buffer_size // 4) / buffer_size
|
||||||
@@ -347,8 +363,8 @@ def get_instance(config):
|
|||||||
CONF_MIRROR_Y,
|
CONF_MIRROR_Y,
|
||||||
CONF_SWAP_XY,
|
CONF_SWAP_XY,
|
||||||
}
|
}
|
||||||
width, height, offset_width, offset_height = model.get_dimensions(
|
width, height, offset_width, offset_height, pad_width, pad_height = (
|
||||||
config, not has_hardware_transform
|
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"))
|
||||||
@@ -374,6 +390,8 @@ def get_instance(config):
|
|||||||
height,
|
height,
|
||||||
offset_width,
|
offset_width,
|
||||||
offset_height,
|
offset_height,
|
||||||
|
pad_width,
|
||||||
|
pad_height,
|
||||||
madctl,
|
madctl,
|
||||||
has_hardware_transform,
|
has_hardware_transform,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -81,10 +81,15 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
|
|||||||
* @tparam HEIGHT Height of the display in pixels
|
* @tparam HEIGHT Height of the display in pixels
|
||||||
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
|
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
|
||||||
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
|
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
|
||||||
|
* @tparam PAD_WIDTH Additional pixels recognised by the controller after the offset and width
|
||||||
|
* @tparam PAD_HEIGHT Additional lines recognised by the controller after the offset and width
|
||||||
|
* @tparam MADCTL The base MADCTL value for the display, with no rotation bits set.
|
||||||
|
* @tparam HAS_HARDWARE_ROTATION Whether the display supports hardware rotation.
|
||||||
* 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, uint16_t MADCTL, bool HAS_HARDWARE_ROTATION>
|
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, int PAD_WIDTH, int PAD_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> {
|
||||||
@@ -126,17 +131,6 @@ class MipiSpi : public display::Display,
|
|||||||
return HEIGHT;
|
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
|
||||||
@@ -233,14 +227,25 @@ class MipiSpi : public display::Display,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dump_config() override {
|
void dump_config() override {
|
||||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT,
|
internal_dump_config(this->model_, this->get_width(), this->get_height(), this->get_offset_width_(),
|
||||||
(uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_,
|
this->get_offset_height_(), (uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8,
|
||||||
this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_, this->mode_,
|
||||||
HAS_HARDWARE_ROTATION);
|
this->data_rate_, BUS_TYPE, HAS_HARDWARE_ROTATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/* METHODS */
|
/* METHODS */
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
// convenience functions to write commands with or without data
|
// convenience functions to write commands with or without data
|
||||||
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
|
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
|
||||||
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
|
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
|
||||||
@@ -330,20 +335,34 @@ class MipiSpi : public display::Display,
|
|||||||
this->write_command_(MADCTL_CMD, madctl);
|
this->write_command_(MADCTL_CMD, madctl);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t get_offset_width_() {
|
uint16_t get_offset_width_() const {
|
||||||
if constexpr (HAS_HARDWARE_ROTATION) {
|
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
switch (this->rotation_) {
|
||||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||||
return OFFSET_HEIGHT;
|
return OFFSET_HEIGHT;
|
||||||
|
case display::DISPLAY_ROTATION_180_DEGREES:
|
||||||
|
return PAD_WIDTH;
|
||||||
|
case display::DISPLAY_ROTATION_270_DEGREES:
|
||||||
|
return PAD_HEIGHT;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return OFFSET_WIDTH;
|
return OFFSET_WIDTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t get_offset_height_() {
|
uint16_t get_offset_height_() const {
|
||||||
if constexpr (HAS_HARDWARE_ROTATION) {
|
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
switch (this->rotation_) {
|
||||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||||
return OFFSET_WIDTH;
|
return PAD_WIDTH;
|
||||||
|
case display::DISPLAY_ROTATION_180_DEGREES:
|
||||||
|
return PAD_HEIGHT;
|
||||||
|
case display::DISPLAY_ROTATION_270_DEGREES:
|
||||||
|
return OFFSET_WIDTH;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return OFFSET_HEIGHT;
|
return OFFSET_HEIGHT;
|
||||||
}
|
}
|
||||||
@@ -396,7 +415,7 @@ class MipiSpi : public display::Display,
|
|||||||
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
|
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (size_t y = 0; y != static_cast<size_t>(h); y++) {
|
for (size_t y = 0; y != h; y++) {
|
||||||
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
|
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
|
||||||
this->write_array(ptr, w);
|
this->write_array(ptr, w);
|
||||||
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||||
@@ -492,19 +511,23 @@ class MipiSpi : public display::Display,
|
|||||||
* @tparam BUFFERPIXEL Color depth of the buffer
|
* @tparam BUFFERPIXEL Color depth of the buffer
|
||||||
* @tparam DISPLAYPIXEL Color depth of the display
|
* @tparam DISPLAYPIXEL Color depth of the display
|
||||||
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
|
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
|
||||||
* @tparam ROTATION The rotation of the display
|
|
||||||
* @tparam WIDTH Width of the display in pixels
|
* @tparam WIDTH Width of the display in pixels
|
||||||
* @tparam HEIGHT Height of the display in pixels
|
* @tparam HEIGHT Height of the display in pixels
|
||||||
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
|
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
|
||||||
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
|
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
|
||||||
|
* @tparam PAD_WIDTH Additional pixels recognised by the controller after the offset and width
|
||||||
|
* @tparam PAD_HEIGHT Additional lines recognised by the controller after the offset and width
|
||||||
|
* @tparam MADCTL The base MADCTL value for the display, with no rotation bits set.
|
||||||
|
* @tparam HAS_HARDWARE_ROTATION Whether the display supports hardware rotation.
|
||||||
* @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
|
* @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
|
||||||
* @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, uint16_t MADCTL,
|
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, int PAD_WIDTH, int PAD_HEIGHT,
|
||||||
bool HAS_HARDWARE_ROTATION, int FRACTION, unsigned ROUNDING>
|
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,
|
class MipiSpiBuffer
|
||||||
OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION> {
|
: public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
||||||
|
OFFSET_HEIGHT, PAD_WIDTH, PAD_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
|
||||||
@@ -515,7 +538,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
|
|
||||||
void dump_config() override {
|
void dump_config() override {
|
||||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||||
MADCTL, HAS_HARDWARE_ROTATION>::dump_config();
|
PAD_WIDTH, PAD_HEIGHT, 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"
|
||||||
@@ -528,7 +551,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
|
|
||||||
void setup() override {
|
void setup() override {
|
||||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||||
MADCTL, HAS_HARDWARE_ROTATION>::setup();
|
PAD_WIDTH, PAD_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION>::setup();
|
||||||
RAMAllocator<BUFFERTYPE> allocator{};
|
RAMAllocator<BUFFERTYPE> allocator{};
|
||||||
this->buffer_ = allocator.allocate(round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION);
|
this->buffer_ = allocator.allocate(round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION);
|
||||||
if (this->buffer_ == nullptr) {
|
if (this->buffer_ == nullptr) {
|
||||||
|
|||||||
@@ -179,6 +179,9 @@ ILI9342 = DriverChip(
|
|||||||
# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation
|
# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation
|
||||||
ILI9341.extend(
|
ILI9341.extend(
|
||||||
"M5CORE2",
|
"M5CORE2",
|
||||||
|
# Reset native dimensions due to axis swap.
|
||||||
|
native_width=320,
|
||||||
|
native_height=240,
|
||||||
width=320,
|
width=320,
|
||||||
height=240,
|
height=240,
|
||||||
mirror_x=False,
|
mirror_x=False,
|
||||||
@@ -786,3 +789,28 @@ ST7796.extend(
|
|||||||
dc_pin=0,
|
dc_pin=0,
|
||||||
invert_colors=True,
|
invert_colors=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ST7789V.extend(
|
||||||
|
"GEEKMAGIC-SMALLTV",
|
||||||
|
data_rate="40MHz",
|
||||||
|
height=240,
|
||||||
|
width=240,
|
||||||
|
offset_width=0,
|
||||||
|
offset_height=0,
|
||||||
|
invert_colors=True,
|
||||||
|
buffer_size=0.125,
|
||||||
|
reset_pin=2,
|
||||||
|
dc_pin=0,
|
||||||
|
)
|
||||||
|
ST7789V.extend(
|
||||||
|
"GEEKMAGIC-SMALLTV-PRO",
|
||||||
|
data_rate="40MHz",
|
||||||
|
height=240,
|
||||||
|
width=240,
|
||||||
|
offset_width=0,
|
||||||
|
offset_height=0,
|
||||||
|
invert_colors=True,
|
||||||
|
buffer_size=0.125,
|
||||||
|
reset_pin=4,
|
||||||
|
dc_pin=2,
|
||||||
|
)
|
||||||
|
|||||||
@@ -269,3 +269,16 @@ ST7789V.extend(
|
|||||||
cs_pin=14,
|
cs_pin=14,
|
||||||
dc_pin={"number": 15, "ignore_strapping_warning": True},
|
dc_pin={"number": 15, "ignore_strapping_warning": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ST7789V.extend(
|
||||||
|
"WAVESHARE-ESP32-S3-GEEK",
|
||||||
|
cs_pin=10,
|
||||||
|
dc_pin=8,
|
||||||
|
reset_pin=9,
|
||||||
|
width=135,
|
||||||
|
height=240,
|
||||||
|
offset_width=52,
|
||||||
|
offset_height=40,
|
||||||
|
invert_colors=True,
|
||||||
|
data_rate="40MHz",
|
||||||
|
)
|
||||||
|
|||||||
@@ -314,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, 0, true, 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, 0, 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
|
||||||
@@ -330,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, 0, true>();"
|
"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, 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
|
||||||
|
|||||||
434
tests/component_tests/mipi_spi/test_padding_and_offsets.py
Normal file
434
tests/component_tests/mipi_spi/test_padding_and_offsets.py
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
"""Tests for padding, offset calculation, and SPI mode configuration in mipi_spi."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
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,
|
||||||
|
MODELS,
|
||||||
|
get_instance,
|
||||||
|
)
|
||||||
|
from esphome.components.spi import CONF_SPI_MODE, TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
||||||
|
from esphome.const import CONF_CS_PIN, CONF_DC_PIN, PlatformFramework
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
from tests.component_tests.types import SetCoreConfigCallable
|
||||||
|
|
||||||
|
|
||||||
|
def validated_config(config: ConfigType) -> ConfigType:
|
||||||
|
"""Run schema + final validation and return the validated config."""
|
||||||
|
config = CONFIG_SCHEMA(config)
|
||||||
|
FINAL_VALIDATE_SCHEMA(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
class TestSPIModeCalculation:
|
||||||
|
"""Test default SPI mode calculation logic."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("bus_mode", "cs_pin", "expected_mode"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
TYPE_OCTAL,
|
||||||
|
None,
|
||||||
|
"MODE3",
|
||||||
|
id="octal_bus_no_cs",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
TYPE_OCTAL,
|
||||||
|
14,
|
||||||
|
"MODE3",
|
||||||
|
id="octal_bus_with_cs",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
TYPE_SINGLE,
|
||||||
|
None,
|
||||||
|
"MODE3",
|
||||||
|
id="single_bus_no_cs",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
TYPE_SINGLE,
|
||||||
|
14,
|
||||||
|
"MODE0",
|
||||||
|
id="single_bus_with_cs",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
TYPE_QUAD,
|
||||||
|
None,
|
||||||
|
"MODE0",
|
||||||
|
id="quad_bus_no_cs",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
TYPE_QUAD,
|
||||||
|
14,
|
||||||
|
"MODE0",
|
||||||
|
id="quad_bus_with_cs",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_default_spi_mode_calculation(
|
||||||
|
self,
|
||||||
|
bus_mode: str,
|
||||||
|
cs_pin: int | None,
|
||||||
|
expected_mode: str,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test that SPI mode is correctly calculated based on bus mode and CS pin."""
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={
|
||||||
|
KEY_BOARD: "esp32-s3-devkitc-1",
|
||||||
|
KEY_VARIANT: VARIANT_ESP32S3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
config: ConfigType = {
|
||||||
|
"model": "custom",
|
||||||
|
"dimensions": {"width": 320, "height": 240},
|
||||||
|
"init_sequence": [[0xA0, 0x01]],
|
||||||
|
"bus_mode": bus_mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add dc_pin for modes that require it (single and octal)
|
||||||
|
# quad mode does not allow dc_pin
|
||||||
|
if bus_mode != TYPE_QUAD:
|
||||||
|
config[CONF_DC_PIN] = 11
|
||||||
|
|
||||||
|
# Add CS pin if specified
|
||||||
|
if cs_pin is not None:
|
||||||
|
config[CONF_CS_PIN] = cs_pin
|
||||||
|
|
||||||
|
validated = validated_config(config)
|
||||||
|
# The validated config should have the correct SPI mode set by model_schema
|
||||||
|
assert validated.get(CONF_SPI_MODE) == expected_mode
|
||||||
|
|
||||||
|
def test_explicit_spi_mode_overrides_default(
|
||||||
|
self,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test that an explicitly configured SPI mode is not overridden."""
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={
|
||||||
|
KEY_BOARD: "esp32-s3-devkitc-1",
|
||||||
|
KEY_VARIANT: VARIANT_ESP32S3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# For octal bus, default is MODE3, but we specify MODE0
|
||||||
|
config = validated_config(
|
||||||
|
{
|
||||||
|
"model": "custom",
|
||||||
|
"dc_pin": 11, # Required for octal mode
|
||||||
|
"dimensions": {"width": 320, "height": 240},
|
||||||
|
"init_sequence": [[0xA0, 0x01]],
|
||||||
|
"bus_mode": TYPE_OCTAL,
|
||||||
|
"spi_mode": "MODE0", # Explicitly set
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert config[CONF_SPI_MODE] == "MODE0"
|
||||||
|
|
||||||
|
|
||||||
|
class TestModelWithPaddingDimensions:
|
||||||
|
"""Test that padding dimensions are correctly returned by models."""
|
||||||
|
|
||||||
|
def test_model_get_dimensions_returns_six_values(
|
||||||
|
self,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test that get_dimensions() returns 6 values including padding."""
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={
|
||||||
|
KEY_BOARD: "esp32-s3-devkitc-1",
|
||||||
|
KEY_VARIANT: VARIANT_ESP32S3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with a real model
|
||||||
|
model = MODELS["ST7735"]
|
||||||
|
config = {"model": "ST7735", "dc_pin": 18}
|
||||||
|
|
||||||
|
# Call get_dimensions - should return 6 values (width, height, offset_x, offset_y, pad_width, pad_height)
|
||||||
|
dimensions = model.get_dimensions(config)
|
||||||
|
assert len(dimensions) == 6
|
||||||
|
assert all(isinstance(v, int) for v in dimensions)
|
||||||
|
|
||||||
|
def test_custom_model_padding_values(
|
||||||
|
self,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test padding values for a custom model with explicit offset."""
|
||||||
|
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": 320,
|
||||||
|
"offset_width": 20,
|
||||||
|
"offset_height": 10,
|
||||||
|
},
|
||||||
|
"init_sequence": [[0xA0, 0x01]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# For custom models, the model is created dynamically from the config
|
||||||
|
# We can verify the config has the right dimensions
|
||||||
|
assert config["dimensions"]["width"] == 240
|
||||||
|
assert config["dimensions"]["height"] == 320
|
||||||
|
assert config["dimensions"]["offset_width"] == 20
|
||||||
|
assert config["dimensions"]["offset_height"] == 10
|
||||||
|
# Padding is not stored in config for custom models (defaults to 0)
|
||||||
|
assert config["dimensions"].get("offset_width_pad", 0) == 0
|
||||||
|
assert config["dimensions"].get("offset_height_pad", 0) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestNewModelVariants:
|
||||||
|
"""Test new model variants added in this change."""
|
||||||
|
|
||||||
|
def test_m5core2_with_native_dimensions(
|
||||||
|
self,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test M5CORE2 variant with reset native_width and native_height."""
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={
|
||||||
|
KEY_BOARD: "esp32-s3-devkitc-1",
|
||||||
|
KEY_VARIANT: VARIANT_ESP32S3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# M5CORE2 should validate successfully
|
||||||
|
config = validated_config({"model": "M5CORE2"})
|
||||||
|
assert config is not None
|
||||||
|
|
||||||
|
# Verify the model has correct dimensions
|
||||||
|
model = MODELS["M5CORE2"]
|
||||||
|
dimensions = model.get_dimensions(config)
|
||||||
|
width, height, _, _, _, _ = dimensions
|
||||||
|
assert width == 320
|
||||||
|
assert height == 240
|
||||||
|
|
||||||
|
def test_geekmagic_smalltv_variant(
|
||||||
|
self,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test GEEKMAGIC-SMALLTV variant of ST7789V."""
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||||
|
)
|
||||||
|
|
||||||
|
# GEEKMAGIC-SMALLTV should validate successfully
|
||||||
|
config = validated_config({"model": "GEEKMAGIC-SMALLTV"})
|
||||||
|
assert config is not None
|
||||||
|
|
||||||
|
# Verify it's a variant of ST7789V with expected dimensions
|
||||||
|
model = MODELS["GEEKMAGIC-SMALLTV"]
|
||||||
|
dimensions = model.get_dimensions(config)
|
||||||
|
width, height, offset_x, offset_y, _, _ = dimensions
|
||||||
|
assert width == 240
|
||||||
|
assert height == 240
|
||||||
|
assert offset_x == 0
|
||||||
|
assert offset_y == 0
|
||||||
|
|
||||||
|
def test_all_predefined_models_with_new_get_dimensions_signature(
|
||||||
|
self,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Verify all predefined models work with new 6-value get_dimensions()."""
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={
|
||||||
|
KEY_BOARD: "esp32-s3-devkitc-1",
|
||||||
|
KEY_VARIANT: VARIANT_ESP32S3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for name, model in MODELS.items():
|
||||||
|
# Skip custom model
|
||||||
|
if name == "custom":
|
||||||
|
continue
|
||||||
|
|
||||||
|
config = {"model": name}
|
||||||
|
|
||||||
|
# Try to get dimensions - should return 6 values for all models
|
||||||
|
dimensions = model.get_dimensions(config)
|
||||||
|
assert len(dimensions) == 6, (
|
||||||
|
f"Model {name} should return 6 dimensions, got {len(dimensions)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTemplateParameterPassing:
|
||||||
|
"""Test that padding parameters are correctly passed to C++ templates."""
|
||||||
|
|
||||||
|
def test_instance_creation_with_padding(
|
||||||
|
self,
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
component_fixture_path: Callable[[str], Path],
|
||||||
|
) -> None:
|
||||||
|
"""Test that get_instance() correctly passes padding parameters to template."""
|
||||||
|
main_cpp = generate_main(component_fixture_path("native.yaml"))
|
||||||
|
|
||||||
|
# native.yaml uses JC3636W518 which should have 8 template parameters for MipiSpiBuffer
|
||||||
|
# (BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE,
|
||||||
|
# WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, PAD_WIDTH, PAD_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION,
|
||||||
|
# FRACTION, ROUNDING)
|
||||||
|
# The instantiation should include padding values (0, 0 for default)
|
||||||
|
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, 0, 0, 0, true, 1, 1>()"
|
||||||
|
in main_cpp
|
||||||
|
), (
|
||||||
|
"Padding parameters (0, 0) should be in the MipiSpiBuffer template instantiation"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_single_mode_with_offset_padding(
|
||||||
|
self,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test that single-mode display with custom offset works with padding."""
|
||||||
|
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": 320,
|
||||||
|
"offset_width": 40,
|
||||||
|
"offset_height": 20,
|
||||||
|
},
|
||||||
|
"init_sequence": [[0xA0, 0x01]],
|
||||||
|
"buffer_size": 0.25,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not raise any errors
|
||||||
|
instance = get_instance(config)
|
||||||
|
assert instance is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserConfiguredPadding:
|
||||||
|
"""Test that pad_width and pad_height can be configured in user dimensions."""
|
||||||
|
|
||||||
|
def test_explicit_pad_width_and_height_in_dimensions(
|
||||||
|
self,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test that pad_width and pad_height can be explicitly set in 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": 240,
|
||||||
|
"height": 320,
|
||||||
|
"offset_width": 40,
|
||||||
|
"offset_height": 20,
|
||||||
|
"pad_width": 80,
|
||||||
|
"pad_height": 40,
|
||||||
|
},
|
||||||
|
"init_sequence": [[0xA0, 0x01]],
|
||||||
|
"buffer_size": 0.25,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Config should validate successfully with padding dimensions
|
||||||
|
assert config is not None
|
||||||
|
assert config["dimensions"]["pad_width"] == 80
|
||||||
|
assert config["dimensions"]["pad_height"] == 40
|
||||||
|
|
||||||
|
def test_padding_for_native_dimension_calculation(
|
||||||
|
self,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test that explicit padding allows native dimensions to be calculated."""
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||||
|
)
|
||||||
|
|
||||||
|
# A controller that has 320x320 total pixels with:
|
||||||
|
# - 240x320 active display area
|
||||||
|
# - offset_width=40, offset_height=20
|
||||||
|
# - pad_width=40 (remaining pixels on right), pad_height=60 (remaining pixels on bottom)
|
||||||
|
config = validated_config(
|
||||||
|
{
|
||||||
|
"model": "custom",
|
||||||
|
"dc_pin": 18,
|
||||||
|
"dimensions": {
|
||||||
|
"width": 240, # Active display width
|
||||||
|
"height": 320, # Active display height
|
||||||
|
"offset_width": 40,
|
||||||
|
"offset_height": 0,
|
||||||
|
"pad_width": 40, # Pixels after width+offset
|
||||||
|
"pad_height": 0, # Pixels after height+offset
|
||||||
|
},
|
||||||
|
"init_sequence": [[0xA0, 0x01]],
|
||||||
|
"buffer_size": 0.25,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get instance should work and correctly calculate native dimensions
|
||||||
|
instance = get_instance(config)
|
||||||
|
assert instance is not None
|
||||||
|
|
||||||
|
def test_padding_without_offset(
|
||||||
|
self,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test padding can be used without offset for controllers with top-left-aligned displays."""
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||||
|
)
|
||||||
|
|
||||||
|
# A display with no offset but padding on right and bottom
|
||||||
|
config = validated_config(
|
||||||
|
{
|
||||||
|
"model": "custom",
|
||||||
|
"dc_pin": 18,
|
||||||
|
"dimensions": {
|
||||||
|
"width": 240,
|
||||||
|
"height": 240,
|
||||||
|
"offset_width": 0,
|
||||||
|
"offset_height": 0,
|
||||||
|
"pad_width": 0,
|
||||||
|
"pad_height": 16,
|
||||||
|
},
|
||||||
|
"init_sequence": [[0xA0, 0x01]],
|
||||||
|
"buffer_size": 0.25,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert config is not None
|
||||||
|
assert config["dimensions"]["width"] == 240
|
||||||
|
assert config["dimensions"]["height"] == 240
|
||||||
|
assert config["dimensions"]["pad_height"] == 16
|
||||||
Reference in New Issue
Block a user