mirror of
https://github.com/esphome/esphome.git
synced 2025-07-29 06:36:45 +00:00
[mipi_spi] Template code, partial buffer support (#9314)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
parent
5480675dd8
commit
6486147da1
@ -3,6 +3,7 @@
|
|||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
|
||||||
CONF_BYTE_ORDER = "byte_order"
|
CONF_BYTE_ORDER = "byte_order"
|
||||||
|
CONF_COLOR_DEPTH = "color_depth"
|
||||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||||
CONF_REQUEST_HEADERS = "request_headers"
|
CONF_REQUEST_HEADERS = "request_headers"
|
||||||
|
@ -2,10 +2,8 @@ CODEOWNERS = ["@clydebarrow"]
|
|||||||
|
|
||||||
DOMAIN = "mipi_spi"
|
DOMAIN = "mipi_spi"
|
||||||
|
|
||||||
CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
|
|
||||||
CONF_SPI_16 = "spi_16"
|
CONF_SPI_16 = "spi_16"
|
||||||
CONF_PIXEL_MODE = "pixel_mode"
|
CONF_PIXEL_MODE = "pixel_mode"
|
||||||
CONF_COLOR_DEPTH = "color_depth"
|
|
||||||
CONF_BUS_MODE = "bus_mode"
|
CONF_BUS_MODE = "bus_mode"
|
||||||
CONF_USE_AXIS_FLIPS = "use_axis_flips"
|
CONF_USE_AXIS_FLIPS = "use_axis_flips"
|
||||||
CONF_NATIVE_WIDTH = "native_width"
|
CONF_NATIVE_WIDTH = "native_width"
|
||||||
|
@ -3,11 +3,18 @@ import logging
|
|||||||
from esphome import pins
|
from esphome import pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import display, spi
|
from esphome.components import display, spi
|
||||||
|
from esphome.components.const import (
|
||||||
|
CONF_BYTE_ORDER,
|
||||||
|
CONF_COLOR_DEPTH,
|
||||||
|
CONF_DRAW_ROUNDING,
|
||||||
|
)
|
||||||
|
from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS
|
||||||
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
from esphome.components.spi import 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 (
|
||||||
CONF_BRIGHTNESS,
|
CONF_BRIGHTNESS,
|
||||||
|
CONF_BUFFER_SIZE,
|
||||||
CONF_COLOR_ORDER,
|
CONF_COLOR_ORDER,
|
||||||
CONF_CS_PIN,
|
CONF_CS_PIN,
|
||||||
CONF_DATA_RATE,
|
CONF_DATA_RATE,
|
||||||
@ -24,19 +31,19 @@ from esphome.const import (
|
|||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
CONF_OFFSET_HEIGHT,
|
CONF_OFFSET_HEIGHT,
|
||||||
CONF_OFFSET_WIDTH,
|
CONF_OFFSET_WIDTH,
|
||||||
|
CONF_PAGES,
|
||||||
CONF_RESET_PIN,
|
CONF_RESET_PIN,
|
||||||
CONF_ROTATION,
|
CONF_ROTATION,
|
||||||
CONF_SWAP_XY,
|
CONF_SWAP_XY,
|
||||||
CONF_TRANSFORM,
|
CONF_TRANSFORM,
|
||||||
CONF_WIDTH,
|
CONF_WIDTH,
|
||||||
)
|
)
|
||||||
from esphome.core import TimePeriod
|
from esphome.core import CORE, TimePeriod
|
||||||
|
from esphome.cpp_generator import TemplateArguments
|
||||||
|
from esphome.final_validate import full_config
|
||||||
|
|
||||||
from ..const import CONF_DRAW_ROUNDING
|
|
||||||
from ..lvgl.defines import CONF_COLOR_DEPTH
|
|
||||||
from . import (
|
from . import (
|
||||||
CONF_BUS_MODE,
|
CONF_BUS_MODE,
|
||||||
CONF_DRAW_FROM_ORIGIN,
|
|
||||||
CONF_NATIVE_HEIGHT,
|
CONF_NATIVE_HEIGHT,
|
||||||
CONF_NATIVE_WIDTH,
|
CONF_NATIVE_WIDTH,
|
||||||
CONF_PIXEL_MODE,
|
CONF_PIXEL_MODE,
|
||||||
@ -55,6 +62,7 @@ from .models import (
|
|||||||
MADCTL_XFLIP,
|
MADCTL_XFLIP,
|
||||||
MADCTL_YFLIP,
|
MADCTL_YFLIP,
|
||||||
DriverChip,
|
DriverChip,
|
||||||
|
adafruit,
|
||||||
amoled,
|
amoled,
|
||||||
cyd,
|
cyd,
|
||||||
ili,
|
ili,
|
||||||
@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"]
|
|||||||
|
|
||||||
LOGGER = logging.getLogger(DOMAIN)
|
LOGGER = logging.getLogger(DOMAIN)
|
||||||
mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi")
|
mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi")
|
||||||
MipiSpi = mipi_spi_ns.class_(
|
MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice)
|
||||||
"MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice
|
MipiSpiBuffer = mipi_spi_ns.class_(
|
||||||
|
"MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice
|
||||||
)
|
)
|
||||||
ColorOrder = display.display_ns.enum("ColorMode")
|
ColorOrder = display.display_ns.enum("ColorMode")
|
||||||
ColorBitness = display.display_ns.enum("ColorBitness")
|
ColorBitness = display.display_ns.enum("ColorBitness")
|
||||||
Model = mipi_spi_ns.enum("Model")
|
Model = mipi_spi_ns.enum("Model")
|
||||||
|
|
||||||
|
PixelMode = mipi_spi_ns.enum("PixelMode")
|
||||||
|
BusType = mipi_spi_ns.enum("BusType")
|
||||||
|
|
||||||
COLOR_ORDERS = {
|
COLOR_ORDERS = {
|
||||||
MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
|
MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
|
||||||
MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
|
MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
|
||||||
}
|
}
|
||||||
|
|
||||||
COLOR_DEPTHS = {
|
COLOR_DEPTHS = {
|
||||||
8: ColorBitness.COLOR_BITNESS_332,
|
8: PixelMode.PIXEL_MODE_8,
|
||||||
16: ColorBitness.COLOR_BITNESS_565,
|
16: PixelMode.PIXEL_MODE_16,
|
||||||
|
18: PixelMode.PIXEL_MODE_18,
|
||||||
}
|
}
|
||||||
|
|
||||||
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
|
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
|
||||||
|
|
||||||
|
BusTypes = {
|
||||||
|
TYPE_SINGLE: BusType.BUS_TYPE_SINGLE,
|
||||||
|
TYPE_QUAD: BusType.BUS_TYPE_QUAD,
|
||||||
|
TYPE_OCTAL: BusType.BUS_TYPE_OCTAL,
|
||||||
|
}
|
||||||
|
|
||||||
DriverChip("CUSTOM", initsequence={})
|
DriverChip("CUSTOM")
|
||||||
|
|
||||||
MODELS = DriverChip.models
|
MODELS = DriverChip.models
|
||||||
# These statements are noops, but serve to suppress linting of side-effect-only imports
|
# This loop is a noop, but suppresses linting of side-effect-only imports
|
||||||
for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare):
|
for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
PixelMode = mipi_spi_ns.enum("PixelMode")
|
|
||||||
|
|
||||||
PIXEL_MODE_18BIT = "18bit"
|
DISPLAY_18BIT = "18bit"
|
||||||
PIXEL_MODE_16BIT = "16bit"
|
DISPLAY_16BIT = "16bit"
|
||||||
|
|
||||||
PIXEL_MODES = {
|
DISPLAY_PIXEL_MODES = {
|
||||||
PIXEL_MODE_16BIT: 0x55,
|
DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16),
|
||||||
PIXEL_MODE_18BIT: 0x66,
|
DISPLAY_18BIT: (0x66, PixelMode.PIXEL_MODE_18),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_dimensions(config):
|
||||||
|
if CONF_DIMENSIONS in config:
|
||||||
|
# Explicit dimensions, just use as is
|
||||||
|
dimensions = config[CONF_DIMENSIONS]
|
||||||
|
if isinstance(dimensions, dict):
|
||||||
|
width = dimensions[CONF_WIDTH]
|
||||||
|
height = dimensions[CONF_HEIGHT]
|
||||||
|
offset_width = dimensions[CONF_OFFSET_WIDTH]
|
||||||
|
offset_height = dimensions[CONF_OFFSET_HEIGHT]
|
||||||
|
return width, height, offset_width, offset_height
|
||||||
|
(width, height) = dimensions
|
||||||
|
return width, height, 0, 0
|
||||||
|
|
||||||
|
# Default dimensions, use model defaults
|
||||||
|
transform = get_transform(config)
|
||||||
|
|
||||||
|
model = MODELS[config[CONF_MODEL]]
|
||||||
|
width = model.get_default(CONF_WIDTH)
|
||||||
|
height = model.get_default(CONF_HEIGHT)
|
||||||
|
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
|
||||||
|
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
|
||||||
|
|
||||||
|
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
|
||||||
|
# the offset is asymmetric
|
||||||
|
if transform[CONF_MIRROR_X]:
|
||||||
|
native_width = model.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2)
|
||||||
|
offset_width = native_width - width - offset_width
|
||||||
|
if transform[CONF_MIRROR_Y]:
|
||||||
|
native_height = model.get_default(
|
||||||
|
CONF_NATIVE_HEIGHT, height + offset_height * 2
|
||||||
|
)
|
||||||
|
offset_height = native_height - height - offset_height
|
||||||
|
# Swap default dimensions if swap_xy is set
|
||||||
|
if transform[CONF_SWAP_XY] is True:
|
||||||
|
width, height = height, width
|
||||||
|
offset_height, offset_width = offset_width, offset_height
|
||||||
|
return width, height, offset_width, offset_height
|
||||||
|
|
||||||
|
|
||||||
|
def denominator(config):
|
||||||
|
"""
|
||||||
|
Calculate the best denominator for a buffer size fraction.
|
||||||
|
The denominator must be a number between 2 and 16 that divides the display height evenly,
|
||||||
|
and the fraction represented by the denominator must be less than or equal to the given fraction.
|
||||||
|
:config: The configuration dictionary containing the buffer size fraction and display dimensions
|
||||||
|
:return: The denominator to use for the buffer size fraction
|
||||||
|
"""
|
||||||
|
frac = config.get(CONF_BUFFER_SIZE)
|
||||||
|
if frac is None or frac > 0.75:
|
||||||
|
return 1
|
||||||
|
height, _width, _offset_width, _offset_height = get_dimensions(config)
|
||||||
|
try:
|
||||||
|
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
|
||||||
|
except StopIteration:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Buffer size fraction {frac} is not compatible with display height {height}"
|
||||||
|
) from StopIteration
|
||||||
|
|
||||||
|
|
||||||
def validate_dimension(rounding):
|
def validate_dimension(rounding):
|
||||||
def validator(value):
|
def validator(value):
|
||||||
value = cv.positive_int(value)
|
value = cv.positive_int(value)
|
||||||
@ -158,41 +235,50 @@ def dimension_schema(rounding):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def model_schema(bus_mode, model: DriverChip, swapsies: bool):
|
def swap_xy_schema(model):
|
||||||
|
uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
|
||||||
|
|
||||||
|
def validator(value):
|
||||||
|
if value:
|
||||||
|
raise cv.Invalid("Axis swapping not supported by this model")
|
||||||
|
return cv.boolean(value)
|
||||||
|
|
||||||
|
if uses_swap:
|
||||||
|
return {cv.Required(CONF_SWAP_XY): cv.boolean}
|
||||||
|
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
|
||||||
|
|
||||||
|
|
||||||
|
def model_schema(config):
|
||||||
|
model = MODELS[config[CONF_MODEL]]
|
||||||
|
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
|
||||||
transform = cv.Schema(
|
transform = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||||
|
**swap_xy_schema(model),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED:
|
|
||||||
transform = transform.extend(
|
|
||||||
{
|
|
||||||
cv.Optional(CONF_SWAP_XY): cv.invalid(
|
|
||||||
"Axis swapping not supported by this model"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
transform = transform.extend(
|
|
||||||
{
|
|
||||||
cv.Required(CONF_SWAP_XY): cv.boolean,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# CUSTOM model will need to provide a custom init sequence
|
# CUSTOM model will need to provide a custom init sequence
|
||||||
iseqconf = (
|
iseqconf = (
|
||||||
cv.Required(CONF_INIT_SEQUENCE)
|
cv.Required(CONF_INIT_SEQUENCE)
|
||||||
if model.initsequence is None
|
if model.initsequence is None
|
||||||
else cv.Optional(CONF_INIT_SEQUENCE)
|
else cv.Optional(CONF_INIT_SEQUENCE)
|
||||||
)
|
)
|
||||||
# Dimensions are optional if the model has a default width and the transform is not overridden
|
# Dimensions are optional if the model has a default width and the x-y transform is not overridden
|
||||||
|
is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
|
||||||
cv_dimensions = (
|
cv_dimensions = (
|
||||||
cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required
|
cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
|
||||||
)
|
)
|
||||||
pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,)
|
pixel_modes = DISPLAY_PIXEL_MODES if bus_mode == TYPE_SINGLE else (DISPLAY_16BIT,)
|
||||||
color_depth = (
|
color_depth = (
|
||||||
("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit")
|
("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit")
|
||||||
)
|
)
|
||||||
|
other_options = [
|
||||||
|
CONF_INVERT_COLORS,
|
||||||
|
CONF_USE_AXIS_FLIPS,
|
||||||
|
]
|
||||||
|
if bus_mode == TYPE_SINGLE:
|
||||||
|
other_options.append(CONF_SPI_16)
|
||||||
schema = (
|
schema = (
|
||||||
display.FULL_DISPLAY_SCHEMA.extend(
|
display.FULL_DISPLAY_SCHEMA.extend(
|
||||||
spi.spi_device_schema(
|
spi.spi_device_schema(
|
||||||
@ -220,11 +306,13 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
|
|||||||
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
|
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
|
||||||
COLOR_ORDERS, upper=True
|
COLOR_ORDERS, upper=True
|
||||||
),
|
),
|
||||||
|
model.option(CONF_BYTE_ORDER, "big_endian"): cv.one_of(
|
||||||
|
"big_endian", "little_endian", lower=True
|
||||||
|
),
|
||||||
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
|
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
|
||||||
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
||||||
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any(
|
model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
|
||||||
cv.one_of(*pixel_modes, lower=True),
|
*pixel_modes, lower=True
|
||||||
cv.int_range(0, 255, min_included=True, max_included=True),
|
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_TRANSFORM): transform,
|
cv.Optional(CONF_TRANSFORM): transform,
|
||||||
cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of(
|
cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of(
|
||||||
@ -232,19 +320,12 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
|
|||||||
),
|
),
|
||||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
||||||
iseqconf: cv.ensure_list(map_sequence),
|
iseqconf: cv.ensure_list(map_sequence),
|
||||||
|
cv.Optional(CONF_BUFFER_SIZE): cv.All(
|
||||||
|
cv.percentage, cv.Range(0.12, 1.0)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(
|
.extend({model.option(x): cv.boolean for x in other_options})
|
||||||
{
|
|
||||||
model.option(x): cv.boolean
|
|
||||||
for x in [
|
|
||||||
CONF_DRAW_FROM_ORIGIN,
|
|
||||||
CONF_SPI_16,
|
|
||||||
CONF_INVERT_COLORS,
|
|
||||||
CONF_USE_AXIS_FLIPS,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if brightness := model.get_default(CONF_BRIGHTNESS):
|
if brightness := model.get_default(CONF_BRIGHTNESS):
|
||||||
schema = schema.extend(
|
schema = schema.extend(
|
||||||
@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
|
|||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
||||||
def rotation_as_transform(model, config):
|
def is_rotation_transformable(config):
|
||||||
"""
|
"""
|
||||||
Check if a rotation can be implemented in hardware using the MADCTL register.
|
Check if a rotation can be implemented in hardware using the MADCTL register.
|
||||||
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
|
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
|
||||||
"""
|
"""
|
||||||
|
model = MODELS[config[CONF_MODEL]]
|
||||||
rotation = config.get(CONF_ROTATION, 0)
|
rotation = config.get(CONF_ROTATION, 0)
|
||||||
return rotation and (
|
return rotation and (
|
||||||
model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
|
model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def config_schema(config):
|
def customise_schema(config):
|
||||||
|
"""
|
||||||
|
Create a customised config schema for a specific model and validate the configuration.
|
||||||
|
:param config: The configuration dictionary to validate
|
||||||
|
:return: The validated configuration dictionary
|
||||||
|
:raises cv.Invalid: If the configuration is invalid
|
||||||
|
"""
|
||||||
# First get the model and bus mode
|
# First get the model and bus mode
|
||||||
config = cv.Schema(
|
config = cv.Schema(
|
||||||
{
|
{
|
||||||
@ -288,29 +376,94 @@ def config_schema(config):
|
|||||||
extra=ALLOW_EXTRA,
|
extra=ALLOW_EXTRA,
|
||||||
)(config)
|
)(config)
|
||||||
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
|
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
|
||||||
swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
|
config = model_schema(config)(config)
|
||||||
config = model_schema(bus_mode, model, swapsies)(config)
|
|
||||||
# Check for invalid combinations of MADCTL config
|
# Check for invalid combinations of MADCTL config
|
||||||
if init_sequence := config.get(CONF_INIT_SEQUENCE):
|
if init_sequence := config.get(CONF_INIT_SEQUENCE):
|
||||||
if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config:
|
commands = [x[0] for x in init_sequence]
|
||||||
|
if MADCTL in commands and CONF_TRANSFORM in config:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
|
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
|
||||||
)
|
)
|
||||||
|
if PIXFMT in commands:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"PIXFMT ({PIXFMT:#X}) should not be in the init sequence, it will be set automatically"
|
||||||
|
)
|
||||||
|
|
||||||
if bus_mode == TYPE_QUAD and CONF_DC_PIN in config:
|
if bus_mode == TYPE_QUAD and CONF_DC_PIN in config:
|
||||||
raise cv.Invalid("DC pin is not supported in quad mode")
|
raise cv.Invalid("DC pin is not supported in quad mode")
|
||||||
if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE:
|
|
||||||
raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus")
|
|
||||||
if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config:
|
if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config:
|
||||||
raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
|
raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
|
||||||
|
denominator(config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = config_schema
|
CONFIG_SCHEMA = customise_schema
|
||||||
|
|
||||||
|
|
||||||
def get_transform(model, config):
|
def requires_buffer(config):
|
||||||
can_transform = rotation_as_transform(model, config)
|
"""
|
||||||
|
Check if the display configuration requires a buffer. It will do so if any drawing methods are configured.
|
||||||
|
:param config:
|
||||||
|
:return: True if a buffer is required, False otherwise
|
||||||
|
"""
|
||||||
|
return any(
|
||||||
|
config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_color_depth(config):
|
||||||
|
return int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
|
||||||
|
|
||||||
|
|
||||||
|
def _final_validate(config):
|
||||||
|
global_config = full_config.get()
|
||||||
|
|
||||||
|
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||||
|
|
||||||
|
if not requires_buffer(config) and LVGL_DOMAIN not in global_config:
|
||||||
|
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||||
|
config[CONF_SHOW_TEST_CARD] = True
|
||||||
|
|
||||||
|
if "psram" not in global_config and CONF_BUFFER_SIZE not in config:
|
||||||
|
if not requires_buffer(config):
|
||||||
|
return config # No buffer needed, so no need to set a buffer size
|
||||||
|
# If PSRAM is not enabled, choose a small buffer size by default
|
||||||
|
if not requires_buffer(config):
|
||||||
|
# not our problem.
|
||||||
|
return config
|
||||||
|
color_depth = get_color_depth(config)
|
||||||
|
frac = denominator(config)
|
||||||
|
height, width, _offset_width, _offset_height = get_dimensions(config)
|
||||||
|
|
||||||
|
buffer_size = color_depth // 8 * width * height // frac
|
||||||
|
# Target a buffer size of 20kB
|
||||||
|
fraction = 20000.0 / buffer_size
|
||||||
|
try:
|
||||||
|
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
||||||
|
x for x in range(2, 17) if fraction >= 1 / x and height % x == 0
|
||||||
|
)
|
||||||
|
except StopIteration:
|
||||||
|
# Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0
|
||||||
|
# PSRAM will be needed.
|
||||||
|
if CORE.is_esp32:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"PSRAM is required for this display"
|
||||||
|
) from StopIteration
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|
||||||
|
|
||||||
|
def get_transform(config):
|
||||||
|
"""
|
||||||
|
Get the transformation configuration for the display.
|
||||||
|
:param config:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
model = MODELS[config[CONF_MODEL]]
|
||||||
|
can_transform = is_rotation_transformable(config)
|
||||||
transform = config.get(
|
transform = config.get(
|
||||||
CONF_TRANSFORM,
|
CONF_TRANSFORM,
|
||||||
{
|
{
|
||||||
@ -350,16 +503,13 @@ def get_sequence(model, config):
|
|||||||
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
|
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
|
||||||
commands = [x[0] for x in sequence]
|
commands = [x[0] for x in sequence]
|
||||||
# Set pixel format if not already in the custom sequence
|
# Set pixel format if not already in the custom sequence
|
||||||
if PIXFMT not in commands:
|
pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]]
|
||||||
pixel_mode = config[CONF_PIXEL_MODE]
|
sequence.append((PIXFMT, pixel_mode[0]))
|
||||||
if not isinstance(pixel_mode, int):
|
|
||||||
pixel_mode = PIXEL_MODES[pixel_mode]
|
|
||||||
sequence.append((PIXFMT, pixel_mode))
|
|
||||||
# Does the chip use the flipping bits for mirroring rather than the reverse order bits?
|
# Does the chip use the flipping bits for mirroring rather than the reverse order bits?
|
||||||
use_flip = config[CONF_USE_AXIS_FLIPS]
|
use_flip = config[CONF_USE_AXIS_FLIPS]
|
||||||
if MADCTL not in commands:
|
if MADCTL not in commands:
|
||||||
madctl = 0
|
madctl = 0
|
||||||
transform = get_transform(model, config)
|
transform = get_transform(config)
|
||||||
if transform.get(CONF_TRANSFORM):
|
if transform.get(CONF_TRANSFORM):
|
||||||
LOGGER.info("Using hardware transform to implement rotation")
|
LOGGER.info("Using hardware transform to implement rotation")
|
||||||
if transform.get(CONF_MIRROR_X):
|
if transform.get(CONF_MIRROR_X):
|
||||||
@ -396,63 +546,62 @@ def get_sequence(model, config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_instance(config):
|
||||||
|
"""
|
||||||
|
Get the type of MipiSpi instance to create based on the configuration,
|
||||||
|
and the template arguments.
|
||||||
|
:param config:
|
||||||
|
:return: type, template arguments
|
||||||
|
"""
|
||||||
|
width, height, offset_width, offset_height = get_dimensions(config)
|
||||||
|
|
||||||
|
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
|
||||||
|
bufferpixels = COLOR_DEPTHS[color_depth]
|
||||||
|
|
||||||
|
display_pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]][1]
|
||||||
|
bus_type = config[CONF_BUS_MODE]
|
||||||
|
if bus_type == TYPE_SINGLE and config.get(CONF_SPI_16, False):
|
||||||
|
# If the bus mode is single and spi_16 is set, use single 16-bit mode
|
||||||
|
bus_type = BusType.BUS_TYPE_SINGLE_16
|
||||||
|
else:
|
||||||
|
bus_type = BusTypes[bus_type]
|
||||||
|
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
|
||||||
|
frac = denominator(config)
|
||||||
|
rotation = DISPLAY_ROTATIONS[
|
||||||
|
0 if is_rotation_transformable(config) else config.get(CONF_ROTATION, 0)
|
||||||
|
]
|
||||||
|
templateargs = [
|
||||||
|
buffer_type,
|
||||||
|
bufferpixels,
|
||||||
|
config[CONF_BYTE_ORDER] == "big_endian",
|
||||||
|
display_pixel_mode,
|
||||||
|
bus_type,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
offset_width,
|
||||||
|
offset_height,
|
||||||
|
]
|
||||||
|
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
|
||||||
|
if requires_buffer(config):
|
||||||
|
templateargs.append(rotation)
|
||||||
|
templateargs.append(frac)
|
||||||
|
return MipiSpiBuffer, templateargs
|
||||||
|
return MipiSpi, templateargs
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
model = MODELS[config[CONF_MODEL]]
|
model = MODELS[config[CONF_MODEL]]
|
||||||
transform = get_transform(model, config)
|
var_id = config[CONF_ID]
|
||||||
if CONF_DIMENSIONS in config:
|
var_id.type, templateargs = get_instance(config)
|
||||||
# Explicit dimensions, just use as is
|
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
|
||||||
dimensions = config[CONF_DIMENSIONS]
|
|
||||||
if isinstance(dimensions, dict):
|
|
||||||
width = dimensions[CONF_WIDTH]
|
|
||||||
height = dimensions[CONF_HEIGHT]
|
|
||||||
offset_width = dimensions[CONF_OFFSET_WIDTH]
|
|
||||||
offset_height = dimensions[CONF_OFFSET_HEIGHT]
|
|
||||||
else:
|
|
||||||
(width, height) = dimensions
|
|
||||||
offset_width = 0
|
|
||||||
offset_height = 0
|
|
||||||
else:
|
|
||||||
# Default dimensions, use model defaults and transform if needed
|
|
||||||
width = model.get_default(CONF_WIDTH)
|
|
||||||
height = model.get_default(CONF_HEIGHT)
|
|
||||||
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
|
|
||||||
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
|
|
||||||
|
|
||||||
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
|
|
||||||
# the offset is asymmetric
|
|
||||||
if transform[CONF_MIRROR_X]:
|
|
||||||
native_width = model.get_default(
|
|
||||||
CONF_NATIVE_WIDTH, width + offset_width * 2
|
|
||||||
)
|
|
||||||
offset_width = native_width - width - offset_width
|
|
||||||
if transform[CONF_MIRROR_Y]:
|
|
||||||
native_height = model.get_default(
|
|
||||||
CONF_NATIVE_HEIGHT, height + offset_height * 2
|
|
||||||
)
|
|
||||||
offset_height = native_height - height - offset_height
|
|
||||||
# Swap default dimensions if swap_xy is set
|
|
||||||
if transform[CONF_SWAP_XY] is True:
|
|
||||||
width, height = height, width
|
|
||||||
offset_height, offset_width = offset_width, offset_height
|
|
||||||
|
|
||||||
color_depth = config[CONF_COLOR_DEPTH]
|
|
||||||
if color_depth.endswith("bit"):
|
|
||||||
color_depth = color_depth[:-3]
|
|
||||||
color_depth = COLOR_DEPTHS[int(color_depth)]
|
|
||||||
|
|
||||||
var = cg.new_Pvariable(
|
|
||||||
config[CONF_ID], width, height, offset_width, offset_height, color_depth
|
|
||||||
)
|
|
||||||
cg.add(var.set_init_sequence(get_sequence(model, config)))
|
cg.add(var.set_init_sequence(get_sequence(model, config)))
|
||||||
if rotation_as_transform(model, config):
|
if is_rotation_transformable(config):
|
||||||
if CONF_TRANSFORM in config:
|
if CONF_TRANSFORM in config:
|
||||||
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
|
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
|
||||||
else:
|
else:
|
||||||
config[CONF_ROTATION] = 0
|
config[CONF_ROTATION] = 0
|
||||||
cg.add(var.set_model(config[CONF_MODEL]))
|
cg.add(var.set_model(config[CONF_MODEL]))
|
||||||
cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN]))
|
|
||||||
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
|
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
|
||||||
cg.add(var.set_spi_16(config[CONF_SPI_16]))
|
|
||||||
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]
|
||||||
cg.add(var.set_enable_pins(enable))
|
cg.add(var.set_enable_pins(enable))
|
||||||
@ -472,4 +621,5 @@ async def to_code(config):
|
|||||||
cg.add(var.set_writer(lambda_))
|
cg.add(var.set_writer(lambda_))
|
||||||
await display.register_display(var, config)
|
await display.register_display(var, config)
|
||||||
await spi.register_spi_device(var, config)
|
await spi.register_spi_device(var, config)
|
||||||
|
# Displays are write-only, set the SPI device to write-only as well
|
||||||
cg.add(var.set_write_only(True))
|
cg.add(var.set_write_only(True))
|
||||||
|
@ -2,489 +2,5 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace mipi_spi {
|
namespace mipi_spi {} // namespace mipi_spi
|
||||||
|
|
||||||
void MipiSpi::setup() {
|
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
this->spi_setup();
|
|
||||||
if (this->dc_pin_ != nullptr) {
|
|
||||||
this->dc_pin_->setup();
|
|
||||||
this->dc_pin_->digital_write(false);
|
|
||||||
}
|
|
||||||
for (auto *pin : this->enable_pins_) {
|
|
||||||
pin->setup();
|
|
||||||
pin->digital_write(true);
|
|
||||||
}
|
|
||||||
if (this->reset_pin_ != nullptr) {
|
|
||||||
this->reset_pin_->setup();
|
|
||||||
this->reset_pin_->digital_write(true);
|
|
||||||
delay(5);
|
|
||||||
this->reset_pin_->digital_write(false);
|
|
||||||
delay(5);
|
|
||||||
this->reset_pin_->digital_write(true);
|
|
||||||
}
|
|
||||||
this->bus_width_ = this->parent_->get_bus_width();
|
|
||||||
|
|
||||||
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
|
|
||||||
auto when = millis() + 120;
|
|
||||||
delay(10);
|
|
||||||
size_t index = 0;
|
|
||||||
auto &vec = this->init_sequence_;
|
|
||||||
while (index != vec.size()) {
|
|
||||||
if (vec.size() - index < 2) {
|
|
||||||
ESP_LOGE(TAG, "Malformed init sequence");
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint8_t cmd = vec[index++];
|
|
||||||
uint8_t x = vec[index++];
|
|
||||||
if (x == DELAY_FLAG) {
|
|
||||||
ESP_LOGD(TAG, "Delay %dms", cmd);
|
|
||||||
delay(cmd);
|
|
||||||
} else {
|
|
||||||
uint8_t num_args = x & 0x7F;
|
|
||||||
if (vec.size() - index < num_args) {
|
|
||||||
ESP_LOGE(TAG, "Malformed init sequence");
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto arg_byte = vec[index];
|
|
||||||
switch (cmd) {
|
|
||||||
case SLEEP_OUT: {
|
|
||||||
// are we ready, boots?
|
|
||||||
int duration = when - millis();
|
|
||||||
if (duration > 0) {
|
|
||||||
ESP_LOGD(TAG, "Sleep %dms", duration);
|
|
||||||
delay(duration);
|
|
||||||
}
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case INVERT_ON:
|
|
||||||
this->invert_colors_ = true;
|
|
||||||
break;
|
|
||||||
case MADCTL_CMD:
|
|
||||||
this->madctl_ = arg_byte;
|
|
||||||
break;
|
|
||||||
case PIXFMT:
|
|
||||||
this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18;
|
|
||||||
break;
|
|
||||||
case BRIGHTNESS:
|
|
||||||
this->brightness_ = arg_byte;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const auto *ptr = vec.data() + index;
|
|
||||||
ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
|
|
||||||
this->write_command_(cmd, ptr, num_args);
|
|
||||||
index += num_args;
|
|
||||||
if (cmd == SLEEP_OUT)
|
|
||||||
delay(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this->setup_complete_ = true;
|
|
||||||
if (this->draw_from_origin_)
|
|
||||||
check_buffer_();
|
|
||||||
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::update() {
|
|
||||||
if (!this->setup_complete_ || this->is_failed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this->do_update_();
|
|
||||||
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
|
|
||||||
return;
|
|
||||||
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
|
|
||||||
// Some chips require that the drawing window be aligned on certain boundaries
|
|
||||||
auto dr = this->draw_rounding_;
|
|
||||||
this->x_low_ = this->x_low_ / dr * dr;
|
|
||||||
this->y_low_ = this->y_low_ / dr * dr;
|
|
||||||
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
|
|
||||||
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
|
|
||||||
if (this->draw_from_origin_) {
|
|
||||||
this->x_low_ = 0;
|
|
||||||
this->y_low_ = 0;
|
|
||||||
this->x_high_ = this->width_ - 1;
|
|
||||||
}
|
|
||||||
int w = this->x_high_ - this->x_low_ + 1;
|
|
||||||
int h = this->y_high_ - this->y_low_ + 1;
|
|
||||||
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_,
|
|
||||||
this->width_ - w - this->x_low_);
|
|
||||||
// invalidate watermarks
|
|
||||||
this->x_low_ = this->width_;
|
|
||||||
this->y_low_ = this->height_;
|
|
||||||
this->x_high_ = 0;
|
|
||||||
this->y_high_ = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::fill(Color color) {
|
|
||||||
if (!this->check_buffer_())
|
|
||||||
return;
|
|
||||||
this->x_low_ = 0;
|
|
||||||
this->y_low_ = 0;
|
|
||||||
this->x_high_ = this->get_width_internal() - 1;
|
|
||||||
this->y_high_ = this->get_height_internal() - 1;
|
|
||||||
switch (this->color_depth_) {
|
|
||||||
case display::COLOR_BITNESS_332: {
|
|
||||||
auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
|
|
||||||
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
auto new_color = display::ColorUtil::color_to_565(color);
|
|
||||||
if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) {
|
|
||||||
// Upper and lower is equal can use quicker memset operation. Takes ~20ms.
|
|
||||||
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
|
|
||||||
} else {
|
|
||||||
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
|
|
||||||
auto len = this->buffer_bytes_ / 2;
|
|
||||||
while (len--) {
|
|
||||||
*ptr_16++ = new_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) {
|
|
||||||
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this->check_buffer_())
|
|
||||||
return;
|
|
||||||
size_t pos = (y * this->width_) + x;
|
|
||||||
switch (this->color_depth_) {
|
|
||||||
case display::COLOR_BITNESS_332: {
|
|
||||||
uint8_t new_color = display::ColorUtil::color_to_332(color);
|
|
||||||
if (this->buffer_[pos] == new_color)
|
|
||||||
return;
|
|
||||||
this->buffer_[pos] = new_color;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case display::COLOR_BITNESS_565: {
|
|
||||||
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
|
|
||||||
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
|
|
||||||
uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3);
|
|
||||||
uint16_t new_color = hi_byte | (lo_byte << 8); // big endian
|
|
||||||
if (ptr_16[pos] == new_color)
|
|
||||||
return;
|
|
||||||
ptr_16[pos] = new_color;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// low and high watermark may speed up drawing from buffer
|
|
||||||
if (x < this->x_low_)
|
|
||||||
this->x_low_ = x;
|
|
||||||
if (y < this->y_low_)
|
|
||||||
this->y_low_ = y;
|
|
||||||
if (x > this->x_high_)
|
|
||||||
this->x_high_ = x;
|
|
||||||
if (y > this->y_high_)
|
|
||||||
this->y_high_ = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::reset_params_() {
|
|
||||||
if (!this->is_ready())
|
|
||||||
return;
|
|
||||||
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
|
||||||
if (this->brightness_.has_value())
|
|
||||||
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_init_sequence_() {
|
|
||||||
size_t index = 0;
|
|
||||||
auto &vec = this->init_sequence_;
|
|
||||||
while (index != vec.size()) {
|
|
||||||
if (vec.size() - index < 2) {
|
|
||||||
ESP_LOGE(TAG, "Malformed init sequence");
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint8_t cmd = vec[index++];
|
|
||||||
uint8_t x = vec[index++];
|
|
||||||
if (x == DELAY_FLAG) {
|
|
||||||
ESP_LOGV(TAG, "Delay %dms", cmd);
|
|
||||||
delay(cmd);
|
|
||||||
} else {
|
|
||||||
uint8_t num_args = x & 0x7F;
|
|
||||||
if (vec.size() - index < num_args) {
|
|
||||||
ESP_LOGE(TAG, "Malformed init sequence");
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto *ptr = vec.data() + index;
|
|
||||||
this->write_command_(cmd, ptr, num_args);
|
|
||||||
index += num_args;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this->setup_complete_ = true;
|
|
||||||
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
|
||||||
ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
|
|
||||||
uint8_t buf[4];
|
|
||||||
x1 += this->offset_width_;
|
|
||||||
x2 += this->offset_width_;
|
|
||||||
y1 += this->offset_height_;
|
|
||||||
y2 += this->offset_height_;
|
|
||||||
put16_be(buf, y1);
|
|
||||||
put16_be(buf + 2, y2);
|
|
||||||
this->write_command_(RASET, buf, sizeof buf);
|
|
||||||
put16_be(buf, x1);
|
|
||||||
put16_be(buf + 2, x2);
|
|
||||||
this->write_command_(CASET, buf, sizeof buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
|
||||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
|
||||||
if (!this->setup_complete_ || this->is_failed())
|
|
||||||
return;
|
|
||||||
if (w <= 0 || h <= 0)
|
|
||||||
return;
|
|
||||||
if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
|
|
||||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this->draw_from_origin_) {
|
|
||||||
auto stride = x_offset + w + x_pad;
|
|
||||||
for (int y = 0; y != h; y++) {
|
|
||||||
memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2,
|
|
||||||
ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2);
|
|
||||||
}
|
|
||||||
ptr = this->buffer_;
|
|
||||||
w = this->width_;
|
|
||||||
h += y_start;
|
|
||||||
x_start = 0;
|
|
||||||
y_start = 0;
|
|
||||||
x_offset = 0;
|
|
||||||
y_offset = 0;
|
|
||||||
}
|
|
||||||
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) {
|
|
||||||
stride -= w;
|
|
||||||
uint8_t transfer_buffer[6 * 256];
|
|
||||||
size_t idx = 0; // index into transfer_buffer
|
|
||||||
while (h-- != 0) {
|
|
||||||
for (auto x = w; x-- != 0;) {
|
|
||||||
auto color_val = *ptr++;
|
|
||||||
// deal with byte swapping
|
|
||||||
transfer_buffer[idx++] = (color_val & 0xF8); // Blue
|
|
||||||
transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11); // Green
|
|
||||||
transfer_buffer[idx++] = (color_val >> 5) & 0xF8; // Red
|
|
||||||
if (idx == sizeof(transfer_buffer)) {
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
idx = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ptr += stride;
|
|
||||||
}
|
|
||||||
if (idx != 0)
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
|
|
||||||
stride -= w;
|
|
||||||
uint8_t transfer_buffer[6 * 256];
|
|
||||||
size_t idx = 0; // index into transfer_buffer
|
|
||||||
while (h-- != 0) {
|
|
||||||
for (auto x = w; x-- != 0;) {
|
|
||||||
auto color_val = *ptr++;
|
|
||||||
transfer_buffer[idx++] = color_val & 0xE0; // Red
|
|
||||||
transfer_buffer[idx++] = (color_val << 3) & 0xE0; // Green
|
|
||||||
transfer_buffer[idx++] = color_val << 6; // Blue
|
|
||||||
if (idx == sizeof(transfer_buffer)) {
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
idx = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ptr += stride;
|
|
||||||
}
|
|
||||||
if (idx != 0)
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
|
|
||||||
stride -= w;
|
|
||||||
uint8_t transfer_buffer[6 * 256];
|
|
||||||
size_t idx = 0; // index into transfer_buffer
|
|
||||||
while (h-- != 0) {
|
|
||||||
for (auto x = w; x-- != 0;) {
|
|
||||||
auto color_val = *ptr++;
|
|
||||||
transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
|
|
||||||
transfer_buffer[idx++] = (color_val & 0x3) << 3;
|
|
||||||
if (idx == sizeof(transfer_buffer)) {
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
idx = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ptr += stride;
|
|
||||||
}
|
|
||||||
if (idx != 0)
|
|
||||||
this->write_array(transfer_buffer, idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
|
||||||
int x_pad) {
|
|
||||||
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
|
|
||||||
auto stride = x_offset + w + x_pad;
|
|
||||||
const auto *offset_ptr = ptr;
|
|
||||||
if (this->color_depth_ == display::COLOR_BITNESS_332) {
|
|
||||||
offset_ptr += y_offset * stride + x_offset;
|
|
||||||
} else {
|
|
||||||
stride *= 2;
|
|
||||||
offset_ptr += y_offset * stride + x_offset * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (this->bus_width_) {
|
|
||||||
case 4:
|
|
||||||
this->enable();
|
|
||||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
|
||||||
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't
|
|
||||||
// bother
|
|
||||||
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4);
|
|
||||||
} else {
|
|
||||||
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4);
|
|
||||||
for (int y = 0; y != h; y++) {
|
|
||||||
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4);
|
|
||||||
offset_ptr += stride;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 8:
|
|
||||||
this->write_command_(WDATA);
|
|
||||||
this->enable();
|
|
||||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
|
||||||
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8);
|
|
||||||
} else {
|
|
||||||
for (int y = 0; y != h; y++) {
|
|
||||||
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8);
|
|
||||||
offset_ptr += stride;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
this->write_command_(WDATA);
|
|
||||||
this->enable();
|
|
||||||
|
|
||||||
if (this->color_depth_ == display::COLOR_BITNESS_565) {
|
|
||||||
// Source buffer is 16-bit RGB565
|
|
||||||
if (this->pixel_mode_ == PIXEL_MODE_18) {
|
|
||||||
// Convert RGB565 to RGB666
|
|
||||||
this->write_18_from_16_bit_(reinterpret_cast<const uint16_t *>(offset_ptr), w, h, stride / 2);
|
|
||||||
} else {
|
|
||||||
// Direct RGB565 output
|
|
||||||
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
|
|
||||||
this->write_array(ptr, w * h * 2);
|
|
||||||
} else {
|
|
||||||
for (int y = 0; y != h; y++) {
|
|
||||||
this->write_array(offset_ptr, w * 2);
|
|
||||||
offset_ptr += stride;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Source buffer is 8-bit RGB332
|
|
||||||
if (this->pixel_mode_ == PIXEL_MODE_18) {
|
|
||||||
// Convert RGB332 to RGB666
|
|
||||||
this->write_18_from_8_bit_(offset_ptr, w, h, stride);
|
|
||||||
} else {
|
|
||||||
this->write_16_from_8_bit_(offset_ptr, w, h, stride);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this->disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
|
||||||
ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
|
|
||||||
if (this->bus_width_ == 4) {
|
|
||||||
this->enable();
|
|
||||||
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
|
||||||
this->disable();
|
|
||||||
} else if (this->bus_width_ == 8) {
|
|
||||||
this->dc_pin_->digital_write(false);
|
|
||||||
this->enable();
|
|
||||||
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
|
|
||||||
this->disable();
|
|
||||||
this->dc_pin_->digital_write(true);
|
|
||||||
if (len != 0) {
|
|
||||||
this->enable();
|
|
||||||
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
|
|
||||||
this->disable();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this->dc_pin_->digital_write(false);
|
|
||||||
this->enable();
|
|
||||||
this->write_byte(cmd);
|
|
||||||
this->disable();
|
|
||||||
this->dc_pin_->digital_write(true);
|
|
||||||
if (len != 0) {
|
|
||||||
if (this->spi_16_) {
|
|
||||||
for (size_t i = 0; i != len; i++) {
|
|
||||||
this->enable();
|
|
||||||
this->write_byte(0);
|
|
||||||
this->write_byte(bytes[i]);
|
|
||||||
this->disable();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this->enable();
|
|
||||||
this->write_array(bytes, len);
|
|
||||||
this->disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MipiSpi::dump_config() {
|
|
||||||
ESP_LOGCONFIG(TAG,
|
|
||||||
"MIPI_SPI Display\n"
|
|
||||||
" Model: %s\n"
|
|
||||||
" Width: %u\n"
|
|
||||||
" Height: %u",
|
|
||||||
this->model_, this->width_, this->height_);
|
|
||||||
if (this->offset_width_ != 0)
|
|
||||||
ESP_LOGCONFIG(TAG, " Offset width: %u", this->offset_width_);
|
|
||||||
if (this->offset_height_ != 0)
|
|
||||||
ESP_LOGCONFIG(TAG, " Offset height: %u", this->offset_height_);
|
|
||||||
ESP_LOGCONFIG(TAG,
|
|
||||||
" Swap X/Y: %s\n"
|
|
||||||
" Mirror X: %s\n"
|
|
||||||
" Mirror Y: %s\n"
|
|
||||||
" Color depth: %d bits\n"
|
|
||||||
" Invert colors: %s\n"
|
|
||||||
" Color order: %s\n"
|
|
||||||
" Pixel mode: %s",
|
|
||||||
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
|
|
||||||
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)),
|
|
||||||
this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8, YESNO(this->invert_colors_),
|
|
||||||
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit");
|
|
||||||
if (this->brightness_.has_value())
|
|
||||||
ESP_LOGCONFIG(TAG, " Brightness: %u", this->brightness_.value());
|
|
||||||
if (this->spi_16_)
|
|
||||||
ESP_LOGCONFIG(TAG, " SPI 16bit: YES");
|
|
||||||
ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_);
|
|
||||||
if (this->draw_from_origin_)
|
|
||||||
ESP_LOGCONFIG(TAG, " Draw from origin: YES");
|
|
||||||
LOG_PIN(" CS Pin: ", this->cs_);
|
|
||||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
|
||||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
|
||||||
ESP_LOGCONFIG(TAG,
|
|
||||||
" SPI Mode: %d\n"
|
|
||||||
" SPI Data rate: %dMHz\n"
|
|
||||||
" SPI Bus width: %d",
|
|
||||||
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), this->bus_width_);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace mipi_spi
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -4,40 +4,39 @@
|
|||||||
|
|
||||||
#include "esphome/components/spi/spi.h"
|
#include "esphome/components/spi/spi.h"
|
||||||
#include "esphome/components/display/display.h"
|
#include "esphome/components/display/display.h"
|
||||||
#include "esphome/components/display/display_buffer.h"
|
|
||||||
#include "esphome/components/display/display_color_utils.h"
|
#include "esphome/components/display/display_color_utils.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace mipi_spi {
|
namespace mipi_spi {
|
||||||
|
|
||||||
constexpr static const char *const TAG = "display.mipi_spi";
|
constexpr static const char *const TAG = "display.mipi_spi";
|
||||||
static const uint8_t SW_RESET_CMD = 0x01;
|
static constexpr uint8_t SW_RESET_CMD = 0x01;
|
||||||
static const uint8_t SLEEP_OUT = 0x11;
|
static constexpr uint8_t SLEEP_OUT = 0x11;
|
||||||
static const uint8_t NORON = 0x13;
|
static constexpr uint8_t NORON = 0x13;
|
||||||
static const uint8_t INVERT_OFF = 0x20;
|
static constexpr uint8_t INVERT_OFF = 0x20;
|
||||||
static const uint8_t INVERT_ON = 0x21;
|
static constexpr uint8_t INVERT_ON = 0x21;
|
||||||
static const uint8_t ALL_ON = 0x23;
|
static constexpr uint8_t ALL_ON = 0x23;
|
||||||
static const uint8_t WRAM = 0x24;
|
static constexpr uint8_t WRAM = 0x24;
|
||||||
static const uint8_t MIPI = 0x26;
|
static constexpr uint8_t MIPI = 0x26;
|
||||||
static const uint8_t DISPLAY_ON = 0x29;
|
static constexpr uint8_t DISPLAY_ON = 0x29;
|
||||||
static const uint8_t RASET = 0x2B;
|
static constexpr uint8_t RASET = 0x2B;
|
||||||
static const uint8_t CASET = 0x2A;
|
static constexpr uint8_t CASET = 0x2A;
|
||||||
static const uint8_t WDATA = 0x2C;
|
static constexpr uint8_t WDATA = 0x2C;
|
||||||
static const uint8_t TEON = 0x35;
|
static constexpr uint8_t TEON = 0x35;
|
||||||
static const uint8_t MADCTL_CMD = 0x36;
|
static constexpr uint8_t MADCTL_CMD = 0x36;
|
||||||
static const uint8_t PIXFMT = 0x3A;
|
static constexpr uint8_t PIXFMT = 0x3A;
|
||||||
static const uint8_t BRIGHTNESS = 0x51;
|
static constexpr uint8_t BRIGHTNESS = 0x51;
|
||||||
static const uint8_t SWIRE1 = 0x5A;
|
static constexpr uint8_t SWIRE1 = 0x5A;
|
||||||
static const uint8_t SWIRE2 = 0x5B;
|
static constexpr uint8_t SWIRE2 = 0x5B;
|
||||||
static const uint8_t PAGESEL = 0xFE;
|
static constexpr uint8_t PAGESEL = 0xFE;
|
||||||
|
|
||||||
static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
||||||
static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
|
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
|
||||||
static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
|
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
|
||||||
static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
|
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
|
||||||
static const 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 const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
||||||
static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
||||||
|
|
||||||
static const uint8_t DELAY_FLAG = 0xFF;
|
static const 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.
|
||||||
@ -46,28 +45,44 @@ static inline void put16_be(uint8_t *buf, uint16_t value) {
|
|||||||
buf[1] = value;
|
buf[1] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buffer mode, conveniently also the number of bytes in a pixel
|
||||||
enum PixelMode {
|
enum PixelMode {
|
||||||
PIXEL_MODE_16,
|
PIXEL_MODE_8 = 1,
|
||||||
PIXEL_MODE_18,
|
PIXEL_MODE_16 = 2,
|
||||||
|
PIXEL_MODE_18 = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
class MipiSpi : public display::DisplayBuffer,
|
enum BusType {
|
||||||
|
BUS_TYPE_SINGLE = 1,
|
||||||
|
BUS_TYPE_QUAD = 4,
|
||||||
|
BUS_TYPE_OCTAL = 8,
|
||||||
|
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for MIPI SPI displays.
|
||||||
|
* All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
|
||||||
|
*
|
||||||
|
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
|
||||||
|
* @tparam BUFFERPIXEL Color depth of the buffer
|
||||||
|
* @tparam DISPLAYPIXEL Color depth of the display
|
||||||
|
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
|
||||||
|
* @tparam WIDTH Width 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_HEIGHT The y-offset of the display in pixels
|
||||||
|
* buffer
|
||||||
|
*/
|
||||||
|
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||||
|
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT>
|
||||||
|
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> {
|
||||||
public:
|
public:
|
||||||
MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth)
|
MipiSpi() {}
|
||||||
: width_(width),
|
void update() override { this->stop_poller(); }
|
||||||
height_(height),
|
void draw_pixel_at(int x, int y, Color color) override {}
|
||||||
offset_width_(offset_width),
|
|
||||||
offset_height_(offset_height),
|
|
||||||
color_depth_(color_depth) {}
|
|
||||||
void set_model(const char *model) { this->model_ = model; }
|
void set_model(const char *model) { this->model_ = model; }
|
||||||
void update() override;
|
|
||||||
void setup() override;
|
|
||||||
display::ColorOrder get_color_mode() {
|
|
||||||
return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB;
|
|
||||||
}
|
|
||||||
|
|
||||||
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
|
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
|
||||||
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
|
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
|
||||||
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
|
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
|
||||||
@ -79,93 +94,524 @@ class MipiSpi : public display::DisplayBuffer,
|
|||||||
this->brightness_ = brightness;
|
this->brightness_ = brightness;
|
||||||
this->reset_params_();
|
this->reset_params_();
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; }
|
|
||||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||||
void dump_config() override;
|
|
||||||
|
|
||||||
int get_width_internal() override { return this->width_; }
|
int get_width_internal() override { return WIDTH; }
|
||||||
int get_height_internal() override { return this->height_; }
|
int get_height_internal() override { return HEIGHT; }
|
||||||
bool can_proceed() override { return this->setup_complete_; }
|
|
||||||
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; }
|
||||||
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
|
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
|
||||||
void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; }
|
|
||||||
|
// reset the display, and write the init sequence
|
||||||
|
void setup() override {
|
||||||
|
this->spi_setup();
|
||||||
|
if (this->dc_pin_ != nullptr) {
|
||||||
|
this->dc_pin_->setup();
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
}
|
||||||
|
for (auto *pin : this->enable_pins_) {
|
||||||
|
pin->setup();
|
||||||
|
pin->digital_write(true);
|
||||||
|
}
|
||||||
|
if (this->reset_pin_ != nullptr) {
|
||||||
|
this->reset_pin_->setup();
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
delay(5);
|
||||||
|
this->reset_pin_->digital_write(false);
|
||||||
|
delay(5);
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
|
||||||
|
auto when = millis() + 120;
|
||||||
|
delay(10);
|
||||||
|
size_t index = 0;
|
||||||
|
auto &vec = this->init_sequence_;
|
||||||
|
while (index != vec.size()) {
|
||||||
|
if (vec.size() - index < 2) {
|
||||||
|
esph_log_e(TAG, "Malformed init sequence");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint8_t cmd = vec[index++];
|
||||||
|
uint8_t x = vec[index++];
|
||||||
|
if (x == DELAY_FLAG) {
|
||||||
|
esph_log_d(TAG, "Delay %dms", cmd);
|
||||||
|
delay(cmd);
|
||||||
|
} else {
|
||||||
|
uint8_t num_args = x & 0x7F;
|
||||||
|
if (vec.size() - index < num_args) {
|
||||||
|
esph_log_e(TAG, "Malformed init sequence");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto arg_byte = vec[index];
|
||||||
|
switch (cmd) {
|
||||||
|
case SLEEP_OUT: {
|
||||||
|
// are we ready, boots?
|
||||||
|
int duration = when - millis();
|
||||||
|
if (duration > 0) {
|
||||||
|
esph_log_d(TAG, "Sleep %dms", duration);
|
||||||
|
delay(duration);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case INVERT_ON:
|
||||||
|
this->invert_colors_ = true;
|
||||||
|
break;
|
||||||
|
case MADCTL_CMD:
|
||||||
|
this->madctl_ = arg_byte;
|
||||||
|
break;
|
||||||
|
case BRIGHTNESS:
|
||||||
|
this->brightness_ = arg_byte;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto *ptr = vec.data() + index;
|
||||||
|
esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
|
||||||
|
this->write_command_(cmd, ptr, num_args);
|
||||||
|
index += num_args;
|
||||||
|
if (cmd == SLEEP_OUT)
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// init sequence no longer needed
|
||||||
|
this->init_sequence_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawing operations
|
||||||
|
|
||||||
|
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||||
|
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override {
|
||||||
|
if (this->is_failed())
|
||||||
|
return;
|
||||||
|
if (w <= 0 || h <= 0)
|
||||||
|
return;
|
||||||
|
if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) {
|
||||||
|
// note that the usual logging macros are banned in header files, so use their replacement
|
||||||
|
esph_log_e(TAG, "Unsupported color depth or bit order");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const BUFFERTYPE *>(ptr), x_offset, y_offset,
|
||||||
|
x_pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dump_config() override {
|
||||||
|
esph_log_config(TAG,
|
||||||
|
"MIPI_SPI Display\n"
|
||||||
|
" Model: %s\n"
|
||||||
|
" Width: %u\n"
|
||||||
|
" Height: %u",
|
||||||
|
this->model_, WIDTH, HEIGHT);
|
||||||
|
if constexpr (OFFSET_WIDTH != 0)
|
||||||
|
esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH);
|
||||||
|
if constexpr (OFFSET_HEIGHT != 0)
|
||||||
|
esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT);
|
||||||
|
esph_log_config(TAG,
|
||||||
|
" Swap X/Y: %s\n"
|
||||||
|
" Mirror X: %s\n"
|
||||||
|
" Mirror Y: %s\n"
|
||||||
|
" Invert colors: %s\n"
|
||||||
|
" Color order: %s\n"
|
||||||
|
" Display pixels: %d bits\n"
|
||||||
|
" Endianness: %s\n",
|
||||||
|
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
|
||||||
|
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_),
|
||||||
|
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little");
|
||||||
|
if (this->brightness_.has_value())
|
||||||
|
esph_log_config(TAG, " Brightness: %u", this->brightness_.value());
|
||||||
|
if (this->cs_ != nullptr)
|
||||||
|
esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str());
|
||||||
|
if (this->reset_pin_ != nullptr)
|
||||||
|
esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str());
|
||||||
|
if (this->dc_pin_ != nullptr)
|
||||||
|
esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str());
|
||||||
|
esph_log_config(TAG,
|
||||||
|
" SPI Mode: %d\n"
|
||||||
|
" SPI Data rate: %dMHz\n"
|
||||||
|
" SPI Bus width: %d",
|
||||||
|
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), BUS_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool check_buffer_() {
|
/* METHODS */
|
||||||
if (this->is_failed())
|
// convenience functions to write commands with or without data
|
||||||
return false;
|
|
||||||
if (this->buffer_ != nullptr)
|
|
||||||
return true;
|
|
||||||
auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1;
|
|
||||||
this->init_internal_(this->width_ * this->height_ * bytes_per_pixel);
|
|
||||||
if (this->buffer_ == nullptr) {
|
|
||||||
this->mark_failed();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
void fill(Color color) override;
|
|
||||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
|
||||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
|
||||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
|
||||||
void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride);
|
|
||||||
void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
|
|
||||||
void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
|
|
||||||
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
|
||||||
int x_pad);
|
|
||||||
/**
|
|
||||||
* the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the
|
|
||||||
* sample code.)
|
|
||||||
*
|
|
||||||
* Immediately after enabling /CS send 4 bytes in single-dataline SPI mode:
|
|
||||||
* 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be
|
|
||||||
* sent in 1-dataline SPI. The second indicates quad mode.
|
|
||||||
* 1: 0x00
|
|
||||||
* 2: The command (register address) byte.
|
|
||||||
* 3: 0x00
|
|
||||||
*
|
|
||||||
* This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte.
|
|
||||||
* At the conclusion of the write, de-assert /CS.
|
|
||||||
*
|
|
||||||
* @param cmd
|
|
||||||
* @param bytes
|
|
||||||
* @param len
|
|
||||||
*/
|
|
||||||
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len);
|
|
||||||
|
|
||||||
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); }
|
||||||
void reset_params_();
|
|
||||||
void write_init_sequence_();
|
|
||||||
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
|
|
||||||
|
|
||||||
|
// Writes a command to the display, with the given bytes.
|
||||||
|
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
||||||
|
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
|
||||||
|
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||||
|
this->enable();
|
||||||
|
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
||||||
|
this->disable();
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
this->enable();
|
||||||
|
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
|
||||||
|
this->disable();
|
||||||
|
this->dc_pin_->digital_write(true);
|
||||||
|
if (len != 0) {
|
||||||
|
this->enable();
|
||||||
|
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) {
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
this->enable();
|
||||||
|
this->write_byte(cmd);
|
||||||
|
this->disable();
|
||||||
|
this->dc_pin_->digital_write(true);
|
||||||
|
if (len != 0) {
|
||||||
|
this->enable();
|
||||||
|
this->write_array(bytes, len);
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) {
|
||||||
|
this->dc_pin_->digital_write(false);
|
||||||
|
this->enable();
|
||||||
|
this->write_byte(cmd);
|
||||||
|
this->disable();
|
||||||
|
this->dc_pin_->digital_write(true);
|
||||||
|
for (size_t i = 0; i != len; i++) {
|
||||||
|
this->enable();
|
||||||
|
this->write_byte(0);
|
||||||
|
this->write_byte(bytes[i]);
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write changed parameters to the display
|
||||||
|
void reset_params_() {
|
||||||
|
if (!this->is_ready())
|
||||||
|
return;
|
||||||
|
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
||||||
|
if (this->brightness_.has_value())
|
||||||
|
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the address window for the next data write
|
||||||
|
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
||||||
|
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
|
||||||
|
uint8_t buf[4];
|
||||||
|
x1 += OFFSET_WIDTH;
|
||||||
|
x2 += OFFSET_WIDTH;
|
||||||
|
y1 += OFFSET_HEIGHT;
|
||||||
|
y2 += OFFSET_HEIGHT;
|
||||||
|
put16_be(buf, y1);
|
||||||
|
put16_be(buf + 2, y2);
|
||||||
|
this->write_command_(RASET, buf, sizeof buf);
|
||||||
|
put16_be(buf, x1);
|
||||||
|
put16_be(buf + 2, x2);
|
||||||
|
this->write_command_(CASET, buf, sizeof buf);
|
||||||
|
if constexpr (BUS_TYPE != BUS_TYPE_QUAD) {
|
||||||
|
this->write_command_(WDATA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// map the display color bitness to the pixel mode
|
||||||
|
static PixelMode get_pixel_mode(display::ColorBitness bitness) {
|
||||||
|
switch (bitness) {
|
||||||
|
case display::COLOR_BITNESS_888:
|
||||||
|
return PIXEL_MODE_18; // 18 bits per pixel
|
||||||
|
case display::COLOR_BITNESS_565:
|
||||||
|
return PIXEL_MODE_16; // 16 bits per pixel
|
||||||
|
default:
|
||||||
|
return PIXEL_MODE_8; // Default to 8 bits per pixel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a buffer to the display.
|
||||||
|
* @param w Width of each line in bytes
|
||||||
|
* @param h Height of the buffer in rows
|
||||||
|
* @param pad Padding in bytes after each line
|
||||||
|
*/
|
||||||
|
void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t pad) {
|
||||||
|
if (pad == 0) {
|
||||||
|
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
|
||||||
|
this->write_array(ptr, w * h);
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||||
|
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h, 4);
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
|
||||||
|
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (size_t y = 0; y != h; y++) {
|
||||||
|
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
|
||||||
|
this->write_array(ptr, w);
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||||
|
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w, 4);
|
||||||
|
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
|
||||||
|
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w, 8);
|
||||||
|
}
|
||||||
|
ptr += w + pad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a buffer to the display.
|
||||||
|
*
|
||||||
|
* The ptr is a pointer to the pixel data
|
||||||
|
* The other parameters are all in pixel units.
|
||||||
|
*/
|
||||||
|
void write_to_display_(int x_start, int y_start, int w, int h, const BUFFERTYPE *ptr, int x_offset, int y_offset,
|
||||||
|
int x_pad) {
|
||||||
|
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
|
||||||
|
this->enable();
|
||||||
|
ptr += y_offset * (x_offset + w + x_pad) + x_offset;
|
||||||
|
if constexpr (BUFFERPIXEL == DISPLAYPIXEL) {
|
||||||
|
this->write_display_data_(reinterpret_cast<const uint8_t *>(ptr), w * sizeof(BUFFERTYPE), h,
|
||||||
|
x_pad * sizeof(BUFFERTYPE));
|
||||||
|
} else {
|
||||||
|
// type conversion required, do it in chunks
|
||||||
|
uint8_t dbuffer[DISPLAYPIXEL * 48];
|
||||||
|
uint8_t *dptr = dbuffer;
|
||||||
|
auto stride = x_offset + w + x_pad; // stride in pixels
|
||||||
|
for (size_t y = 0; y != h; y++) {
|
||||||
|
for (size_t x = 0; x != w; x++) {
|
||||||
|
auto color_val = ptr[y * stride + x];
|
||||||
|
if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) {
|
||||||
|
// 16 to 18 bit conversion
|
||||||
|
if constexpr (IS_BIG_ENDIAN) {
|
||||||
|
*dptr++ = color_val & 0xF8;
|
||||||
|
*dptr++ = ((color_val & 0x7) << 5) | (color_val & 0xE000) >> 11;
|
||||||
|
*dptr++ = (color_val >> 5) & 0xF8;
|
||||||
|
} else {
|
||||||
|
*dptr++ = (color_val >> 8) & 0xF8; // Blue
|
||||||
|
*dptr++ = (color_val & 0x7E0) >> 3;
|
||||||
|
*dptr++ = color_val << 3;
|
||||||
|
}
|
||||||
|
} else if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_8) {
|
||||||
|
// 8 bit to 18 bit conversion
|
||||||
|
*dptr++ = color_val << 6; // Blue
|
||||||
|
*dptr++ = (color_val & 0x1C) << 3; // Green
|
||||||
|
*dptr++ = (color_val & 0xE0); // Red
|
||||||
|
} else if constexpr (DISPLAYPIXEL == PIXEL_MODE_16 && BUFFERPIXEL == PIXEL_MODE_8) {
|
||||||
|
if constexpr (IS_BIG_ENDIAN) {
|
||||||
|
*dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
|
||||||
|
*dptr++ = (color_val & 3) << 3;
|
||||||
|
} else {
|
||||||
|
*dptr++ = (color_val & 3) << 3;
|
||||||
|
*dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// buffer full? Flush.
|
||||||
|
if (dptr == dbuffer + sizeof(dbuffer)) {
|
||||||
|
this->write_display_data_(dbuffer, sizeof(dbuffer), 1, 0);
|
||||||
|
dptr = dbuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// flush any remaining data
|
||||||
|
if (dptr != dbuffer) {
|
||||||
|
this->write_display_data_(dbuffer, dptr - dbuffer, 1, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PROPERTIES */
|
||||||
|
|
||||||
|
// GPIO pins
|
||||||
GPIOPin *reset_pin_{nullptr};
|
GPIOPin *reset_pin_{nullptr};
|
||||||
std::vector<GPIOPin *> enable_pins_{};
|
std::vector<GPIOPin *> enable_pins_{};
|
||||||
GPIOPin *dc_pin_{nullptr};
|
GPIOPin *dc_pin_{nullptr};
|
||||||
uint16_t x_low_{1};
|
|
||||||
uint16_t y_low_{1};
|
|
||||||
uint16_t x_high_{0};
|
|
||||||
uint16_t y_high_{0};
|
|
||||||
bool setup_complete_{};
|
|
||||||
|
|
||||||
|
// other properties set by configuration
|
||||||
bool invert_colors_{};
|
bool invert_colors_{};
|
||||||
size_t width_;
|
|
||||||
size_t height_;
|
|
||||||
int16_t offset_width_;
|
|
||||||
int16_t offset_height_;
|
|
||||||
size_t buffer_bytes_{0};
|
|
||||||
display::ColorBitness color_depth_;
|
|
||||||
PixelMode pixel_mode_{PIXEL_MODE_16};
|
|
||||||
uint8_t bus_width_{};
|
|
||||||
bool spi_16_{};
|
|
||||||
uint8_t madctl_{};
|
|
||||||
bool draw_from_origin_{false};
|
|
||||||
unsigned draw_rounding_{2};
|
unsigned draw_rounding_{2};
|
||||||
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_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for MIPI SPI displays with a buffer.
|
||||||
|
*
|
||||||
|
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
|
||||||
|
* @tparam BUFFERPIXEL Color depth of the buffer
|
||||||
|
* @tparam DISPLAYPIXEL Color depth of the display
|
||||||
|
* @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 HEIGHT Height 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 FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
|
||||||
|
*/
|
||||||
|
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||||
|
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, int FRACTION>
|
||||||
|
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
|
||||||
|
OFFSET_WIDTH, OFFSET_HEIGHT> {
|
||||||
|
public:
|
||||||
|
MipiSpiBuffer() { this->rotation_ = ROTATION; }
|
||||||
|
|
||||||
|
void dump_config() override {
|
||||||
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
||||||
|
OFFSET_HEIGHT>::dump_config();
|
||||||
|
esph_log_config(TAG,
|
||||||
|
" Rotation: %d°\n"
|
||||||
|
" Buffer pixels: %d bits\n"
|
||||||
|
" Buffer fraction: 1/%d\n"
|
||||||
|
" Buffer bytes: %zu\n"
|
||||||
|
" Draw rounding: %u",
|
||||||
|
this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION,
|
||||||
|
this->draw_rounding_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() override {
|
||||||
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
||||||
|
OFFSET_HEIGHT>::setup();
|
||||||
|
RAMAllocator<BUFFERTYPE> allocator{};
|
||||||
|
this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION);
|
||||||
|
if (this->buffer_ == nullptr) {
|
||||||
|
this->mark_failed("Buffer allocation failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() override {
|
||||||
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
auto now = millis();
|
||||||
|
#endif
|
||||||
|
if (this->is_failed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
|
||||||
|
// the display height,
|
||||||
|
for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) {
|
||||||
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
auto lap = millis();
|
||||||
|
#endif
|
||||||
|
this->end_line_ = this->start_line_ + HEIGHT / FRACTION;
|
||||||
|
if (this->auto_clear_enabled_) {
|
||||||
|
this->clear();
|
||||||
|
}
|
||||||
|
if (this->page_ != nullptr) {
|
||||||
|
this->page_->get_writer()(*this);
|
||||||
|
} else if (this->writer_.has_value()) {
|
||||||
|
(*this->writer_)(*this);
|
||||||
|
} else {
|
||||||
|
this->test_card();
|
||||||
|
}
|
||||||
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
esph_log_v(TAG, "Drawing from line %d took %dms", this->start_line_, millis() - lap);
|
||||||
|
lap = millis();
|
||||||
|
#endif
|
||||||
|
if (this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
|
||||||
|
return;
|
||||||
|
esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_,
|
||||||
|
this->y_high_);
|
||||||
|
// Some chips require that the drawing window be aligned on certain boundaries
|
||||||
|
auto dr = this->draw_rounding_;
|
||||||
|
this->x_low_ = this->x_low_ / dr * dr;
|
||||||
|
this->y_low_ = this->y_low_ / dr * dr;
|
||||||
|
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
|
||||||
|
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
|
||||||
|
int w = this->x_high_ - this->x_low_ + 1;
|
||||||
|
int h = this->y_high_ - this->y_low_ + 1;
|
||||||
|
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
||||||
|
this->y_low_ - this->start_line_, WIDTH - w);
|
||||||
|
// invalidate watermarks
|
||||||
|
this->x_low_ = WIDTH;
|
||||||
|
this->y_low_ = HEIGHT;
|
||||||
|
this->x_high_ = 0;
|
||||||
|
this->y_high_ = 0;
|
||||||
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
esph_log_v(TAG, "Write to display took %dms", millis() - lap);
|
||||||
|
lap = millis();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
|
esph_log_v(TAG, "Total update took %dms", millis() - now);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a pixel at the given coordinates.
|
||||||
|
void draw_pixel_at(int x, int y, Color color) override {
|
||||||
|
rotate_coordinates_(x, y);
|
||||||
|
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
|
||||||
|
return;
|
||||||
|
this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color);
|
||||||
|
if (x < this->x_low_) {
|
||||||
|
this->x_low_ = x;
|
||||||
|
}
|
||||||
|
if (x > this->x_high_) {
|
||||||
|
this->x_high_ = x;
|
||||||
|
}
|
||||||
|
if (y < this->y_low_) {
|
||||||
|
this->y_low_ = y;
|
||||||
|
}
|
||||||
|
if (y > this->y_high_) {
|
||||||
|
this->y_high_ = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fills the display with a color.
|
||||||
|
void fill(Color color) override {
|
||||||
|
this->x_low_ = 0;
|
||||||
|
this->y_low_ = this->start_line_;
|
||||||
|
this->x_high_ = WIDTH - 1;
|
||||||
|
this->y_high_ = this->end_line_ - 1;
|
||||||
|
std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_width() override {
|
||||||
|
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
|
||||||
|
return HEIGHT;
|
||||||
|
return WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_height() override {
|
||||||
|
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
|
||||||
|
return WIDTH;
|
||||||
|
return HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Rotate the coordinates to match the display orientation.
|
||||||
|
void rotate_coordinates_(int &x, int &y) const {
|
||||||
|
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.
|
||||||
|
BUFFERTYPE convert_color_(Color &color) const {
|
||||||
|
if constexpr (BUFFERPIXEL == PIXEL_MODE_8) {
|
||||||
|
return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6;
|
||||||
|
} else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) {
|
||||||
|
if constexpr (IS_BIG_ENDIAN) {
|
||||||
|
return (color.r & 0xF8) | color.g >> 5 | (color.g & 0x1C) << 11 | (color.b & 0xF8) << 5;
|
||||||
|
} else {
|
||||||
|
return (color.r & 0xF8) << 8 | (color.g & 0xFC) << 3 | color.b >> 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<BUFFERTYPE>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
BUFFERTYPE *buffer_{};
|
||||||
|
uint16_t x_low_{WIDTH};
|
||||||
|
uint16_t y_low_{HEIGHT};
|
||||||
|
uint16_t x_high_{0};
|
||||||
|
uint16_t y_high_{0};
|
||||||
|
uint16_t start_line_{0};
|
||||||
|
uint16_t end_line_{1};
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace mipi_spi
|
} // namespace mipi_spi
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
30
esphome/components/mipi_spi/models/adafruit.py
Normal file
30
esphome/components/mipi_spi/models/adafruit.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from .ili import ST7789V
|
||||||
|
|
||||||
|
ST7789V.extend(
|
||||||
|
"ADAFRUIT-FUNHOUSE",
|
||||||
|
height=240,
|
||||||
|
width=240,
|
||||||
|
offset_height=0,
|
||||||
|
offset_width=0,
|
||||||
|
cs_pin=40,
|
||||||
|
dc_pin=39,
|
||||||
|
reset_pin=41,
|
||||||
|
invert_colors=True,
|
||||||
|
mirror_x=True,
|
||||||
|
mirror_y=True,
|
||||||
|
data_rate="80MHz",
|
||||||
|
)
|
||||||
|
|
||||||
|
ST7789V.extend(
|
||||||
|
"ADAFRUIT-S2-TFT-FEATHER",
|
||||||
|
height=240,
|
||||||
|
width=135,
|
||||||
|
offset_height=52,
|
||||||
|
offset_width=40,
|
||||||
|
cs_pin=7,
|
||||||
|
dc_pin=39,
|
||||||
|
reset_pin=40,
|
||||||
|
invert_colors=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
models = {}
|
@ -67,6 +67,14 @@ RM690B0 = DriverChip(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD)
|
T4_S3_AMOLED = RM690B0.extend(
|
||||||
|
"T4-S3",
|
||||||
|
width=450,
|
||||||
|
offset_width=16,
|
||||||
|
cs_pin=11,
|
||||||
|
reset_pin=13,
|
||||||
|
enable_pin=9,
|
||||||
|
bus_mode=TYPE_QUAD,
|
||||||
|
)
|
||||||
|
|
||||||
models = {}
|
models = {}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
from . import DriverChip
|
from . import DriverChip
|
||||||
from .ili import ILI9488_A
|
from .ili import ILI9488_A
|
||||||
|
|
||||||
@ -128,6 +130,7 @@ DriverChip(
|
|||||||
|
|
||||||
ILI9488_A.extend(
|
ILI9488_A.extend(
|
||||||
"PICO-RESTOUCH-LCD-3.5",
|
"PICO-RESTOUCH-LCD-3.5",
|
||||||
|
swap_xy=cv.UNDEFINED,
|
||||||
spi_16=True,
|
spi_16=True,
|
||||||
pixel_mode="16bit",
|
pixel_mode="16bit",
|
||||||
mirror_x=True,
|
mirror_x=True,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from esphome import pins
|
from esphome import pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
@ -139,6 +140,27 @@ def get_hw_interface_list():
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def one_of_interface_validator(additional_values: list[str] | None = None) -> Any:
|
||||||
|
"""Helper to create a one_of validator for SPI interfaces.
|
||||||
|
|
||||||
|
This delays evaluation of get_hw_interface_list() until validation time,
|
||||||
|
avoiding access to CORE.data during module import.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
additional_values: List of additional valid values to include
|
||||||
|
"""
|
||||||
|
if additional_values is None:
|
||||||
|
additional_values = []
|
||||||
|
|
||||||
|
def validator(value: str) -> str:
|
||||||
|
return cv.one_of(
|
||||||
|
*sum(get_hw_interface_list(), additional_values),
|
||||||
|
lower=True,
|
||||||
|
)(value)
|
||||||
|
|
||||||
|
return cv.All(cv.string, validator)
|
||||||
|
|
||||||
|
|
||||||
# Given an SPI name, return the index of it in the available list
|
# Given an SPI name, return the index of it in the available list
|
||||||
def get_spi_index(name):
|
def get_spi_index(name):
|
||||||
for i, ilist in enumerate(get_hw_interface_list()):
|
for i, ilist in enumerate(get_hw_interface_list()):
|
||||||
@ -274,9 +296,8 @@ SPI_SINGLE_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_FORCE_SW): cv.invalid(
|
cv.Optional(CONF_FORCE_SW): cv.invalid(
|
||||||
"force_sw is deprecated - use interface: software"
|
"force_sw is deprecated - use interface: software"
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_INTERFACE, default="any"): cv.one_of(
|
cv.Optional(CONF_INTERFACE, default="any"): one_of_interface_validator(
|
||||||
*sum(get_hw_interface_list(), ["software", "hardware", "any"]),
|
["software", "hardware", "any"]
|
||||||
lower=True,
|
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_DATA_PINS): cv.invalid(
|
cv.Optional(CONF_DATA_PINS): cv.invalid(
|
||||||
"'data_pins' should be used with 'type: quad or octal' only"
|
"'data_pins' should be used with 'type: quad or octal' only"
|
||||||
@ -309,10 +330,9 @@ def spi_mode_schema(mode):
|
|||||||
cv.ensure_list(pins.internal_gpio_output_pin_number),
|
cv.ensure_list(pins.internal_gpio_output_pin_number),
|
||||||
cv.Length(min=pin_count, max=pin_count),
|
cv.Length(min=pin_count, max=pin_count),
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of(
|
cv.Optional(
|
||||||
*sum(get_hw_interface_list(), ["hardware"]),
|
CONF_INTERFACE, default="hardware"
|
||||||
lower=True,
|
): one_of_interface_validator(["hardware"]),
|
||||||
),
|
|
||||||
cv.Optional(CONF_MISO_PIN): cv.invalid(
|
cv.Optional(CONF_MISO_PIN): cv.invalid(
|
||||||
f"'miso_pin' should not be used with {mode} SPI"
|
f"'miso_pin' should not be used with {mode} SPI"
|
||||||
),
|
),
|
||||||
|
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""ESPHome tests package."""
|
0
tests/component_tests/__init__.py
Normal file
0
tests/component_tests/__init__.py
Normal file
0
tests/component_tests/binary_sensor/__init__.py
Normal file
0
tests/component_tests/binary_sensor/__init__.py
Normal file
0
tests/component_tests/button/__init__.py
Normal file
0
tests/component_tests/button/__init__.py
Normal file
@ -5,18 +5,30 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable, Generator
|
from collections.abc import Callable, Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from esphome import config, final_validate
|
||||||
|
from esphome.const import (
|
||||||
|
KEY_CORE,
|
||||||
|
KEY_TARGET_FRAMEWORK,
|
||||||
|
KEY_TARGET_PLATFORM,
|
||||||
|
PlatformFramework,
|
||||||
|
)
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
# Add package root to python path
|
# Add package root to python path
|
||||||
here = Path(__file__).parent
|
here = Path(__file__).parent
|
||||||
package_root = here.parent.parent
|
package_root = here.parent.parent
|
||||||
sys.path.insert(0, package_root.as_posix())
|
sys.path.insert(0, package_root.as_posix())
|
||||||
|
|
||||||
from esphome.__main__ import generate_cpp_contents # noqa: E402
|
from esphome.__main__ import generate_cpp_contents # noqa: E402
|
||||||
from esphome.config import read_config # noqa: E402
|
from esphome.config import Config, read_config # noqa: E402
|
||||||
from esphome.core import CORE # noqa: E402
|
from esphome.core import CORE # noqa: E402
|
||||||
|
|
||||||
|
from .types import SetCoreConfigCallable # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def config_path(request: pytest.FixtureRequest) -> Generator[None]:
|
def config_path(request: pytest.FixtureRequest) -> Generator[None]:
|
||||||
@ -36,6 +48,59 @@ def config_path(request: pytest.FixtureRequest) -> Generator[None]:
|
|||||||
CORE.config_path = original_path
|
CORE.config_path = original_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_core() -> Generator[None]:
|
||||||
|
"""Reset CORE after each test."""
|
||||||
|
yield
|
||||||
|
CORE.reset()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def set_core_config() -> Generator[SetCoreConfigCallable]:
|
||||||
|
"""Fixture to set up the core configuration for tests."""
|
||||||
|
|
||||||
|
def setter(
|
||||||
|
platform_framework: PlatformFramework,
|
||||||
|
/,
|
||||||
|
*,
|
||||||
|
core_data: ConfigType | None = None,
|
||||||
|
platform_data: ConfigType | None = None,
|
||||||
|
) -> None:
|
||||||
|
platform, framework = platform_framework.value
|
||||||
|
|
||||||
|
# Set base core configuration
|
||||||
|
CORE.data[KEY_CORE] = {
|
||||||
|
KEY_TARGET_PLATFORM: platform.value,
|
||||||
|
KEY_TARGET_FRAMEWORK: framework.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update with any additional core data
|
||||||
|
if core_data:
|
||||||
|
CORE.data[KEY_CORE].update(core_data)
|
||||||
|
|
||||||
|
# Set platform-specific data
|
||||||
|
if platform_data:
|
||||||
|
CORE.data[platform.value] = platform_data
|
||||||
|
|
||||||
|
config.path_context.set([])
|
||||||
|
final_validate.full_config.set(Config())
|
||||||
|
|
||||||
|
yield setter
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def set_component_config() -> Callable[[str, Any], None]:
|
||||||
|
"""
|
||||||
|
Fixture to set a component configuration in the mock config.
|
||||||
|
This must be used after the core configuration has been set up.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setter(name: str, value: Any) -> None:
|
||||||
|
final_validate.full_config.get()[name] = value
|
||||||
|
|
||||||
|
return setter
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
|
def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
|
||||||
"""Return a function to get absolute paths relative to the component's fixtures directory."""
|
"""Return a function to get absolute paths relative to the component's fixtures directory."""
|
||||||
@ -60,7 +125,7 @@ def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Pat
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def generate_main() -> Generator[Callable[[str | Path], str]]:
|
def generate_main() -> Generator[Callable[[str | Path], str]]:
|
||||||
"""Generates the C++ main.cpp file and returns it in string form."""
|
"""Generates the C++ main.cpp from a given yaml file and returns it in string form."""
|
||||||
|
|
||||||
def generator(path: str | Path) -> str:
|
def generator(path: str | Path) -> str:
|
||||||
CORE.config_path = str(path)
|
CORE.config_path = str(path)
|
||||||
@ -69,5 +134,3 @@ def generate_main() -> Generator[Callable[[str | Path], str]]:
|
|||||||
return CORE.cpp_main_section
|
return CORE.cpp_main_section
|
||||||
|
|
||||||
yield generator
|
yield generator
|
||||||
|
|
||||||
CORE.reset()
|
|
||||||
|
0
tests/component_tests/deep_sleep/__init__.py
Normal file
0
tests/component_tests/deep_sleep/__init__.py
Normal file
0
tests/component_tests/image/__init__.py
Normal file
0
tests/component_tests/image/__init__.py
Normal file
0
tests/component_tests/mipi_spi/__init__.py
Normal file
0
tests/component_tests/mipi_spi/__init__.py
Normal file
25
tests/component_tests/mipi_spi/fixtures/lvgl.yaml
Normal file
25
tests/component_tests/mipi_spi/fixtures/lvgl.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
esphome:
|
||||||
|
name: c3-7735
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: lolin_c3_mini
|
||||||
|
|
||||||
|
spi:
|
||||||
|
mosi_pin:
|
||||||
|
number: GPIO2
|
||||||
|
ignore_strapping_warning: true
|
||||||
|
clk_pin: GPIO1
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: mipi_spi
|
||||||
|
data_rate: 20MHz
|
||||||
|
model: st7735
|
||||||
|
cs_pin:
|
||||||
|
number: GPIO8
|
||||||
|
ignore_strapping_warning: true
|
||||||
|
dc_pin:
|
||||||
|
number: GPIO3
|
||||||
|
reset_pin:
|
||||||
|
number: GPIO4
|
||||||
|
|
||||||
|
lvgl:
|
20
tests/component_tests/mipi_spi/fixtures/native.yaml
Normal file
20
tests/component_tests/mipi_spi/fixtures/native.yaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
esphome:
|
||||||
|
name: jc3636w518
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32-s3-devkitc-1
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
|
psram:
|
||||||
|
mode: octal
|
||||||
|
|
||||||
|
spi:
|
||||||
|
id: display_qspi
|
||||||
|
type: quad
|
||||||
|
clk_pin: 9
|
||||||
|
data_pins: [11, 12, 13, 14]
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: mipi_spi
|
||||||
|
model: jc3636w518
|
387
tests/component_tests/mipi_spi/test_init.py
Normal file
387
tests/component_tests/mipi_spi/test_init.py
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
"""Tests for mpip_spi configuration validation."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from esphome import config_validation as cv
|
||||||
|
from esphome.components.esp32 import (
|
||||||
|
KEY_BOARD,
|
||||||
|
KEY_ESP32,
|
||||||
|
KEY_VARIANT,
|
||||||
|
VARIANT_ESP32,
|
||||||
|
VARIANT_ESP32S3,
|
||||||
|
VARIANTS,
|
||||||
|
)
|
||||||
|
from esphome.components.esp32.gpio import validate_gpio_pin
|
||||||
|
from esphome.components.mipi_spi.display import (
|
||||||
|
CONF_BUS_MODE,
|
||||||
|
CONF_NATIVE_HEIGHT,
|
||||||
|
CONFIG_SCHEMA,
|
||||||
|
FINAL_VALIDATE_SCHEMA,
|
||||||
|
MODELS,
|
||||||
|
dimension_schema,
|
||||||
|
)
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_DC_PIN,
|
||||||
|
CONF_DIMENSIONS,
|
||||||
|
CONF_HEIGHT,
|
||||||
|
CONF_INIT_SEQUENCE,
|
||||||
|
CONF_WIDTH,
|
||||||
|
PlatformFramework,
|
||||||
|
)
|
||||||
|
from esphome.core import CORE
|
||||||
|
from esphome.pins import internal_gpio_pin_number
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
from tests.component_tests.types import SetCoreConfigCallable
|
||||||
|
|
||||||
|
|
||||||
|
def run_schema_validation(config: ConfigType) -> None:
|
||||||
|
"""Run schema validation on a configuration."""
|
||||||
|
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def choose_variant_with_pins() -> Callable[..., None]:
|
||||||
|
"""
|
||||||
|
Set the ESP32 variant for the given model based on pins. For ESP32 only since the other platforms
|
||||||
|
do not have variants.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def chooser(*pins: int | str | None) -> None:
|
||||||
|
for v in VARIANTS:
|
||||||
|
try:
|
||||||
|
CORE.data[KEY_ESP32][KEY_VARIANT] = v
|
||||||
|
for pin in pins:
|
||||||
|
if pin is not None:
|
||||||
|
pin = internal_gpio_pin_number(pin)
|
||||||
|
validate_gpio_pin(pin)
|
||||||
|
return
|
||||||
|
except cv.Invalid:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return chooser
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("config", "error_match"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
"a string",
|
||||||
|
"expected a dictionary",
|
||||||
|
id="invalid_string_config",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"id": "display_id"},
|
||||||
|
r"required key not provided @ data\['model'\]",
|
||||||
|
id="missing_model",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"id": "display_id", "model": "custom", "init_sequence": [[0x36, 0x01]]},
|
||||||
|
r"required key not provided @ data\['dimensions'\]",
|
||||||
|
id="missing_dimensions",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"model": "custom",
|
||||||
|
"dc_pin": 18,
|
||||||
|
"dimensions": {"width": 320, "height": 240},
|
||||||
|
},
|
||||||
|
r"required key not provided @ data\['init_sequence'\]",
|
||||||
|
id="missing_init_sequence",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"id": "display_id",
|
||||||
|
"model": "custom",
|
||||||
|
"dimensions": {"width": 320, "height": 240},
|
||||||
|
"draw_rounding": 13,
|
||||||
|
"init_sequence": [[0xA0, 0x01]],
|
||||||
|
},
|
||||||
|
r"value must be a power of two for dictionary value @ data\['draw_rounding'\]",
|
||||||
|
id="invalid_draw_rounding",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_basic_configuration_errors(
|
||||||
|
config: str | ConfigType,
|
||||||
|
error_match: str,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test basic configuration validation errors"""
|
||||||
|
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(cv.Invalid, match=error_match):
|
||||||
|
run_schema_validation(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("rounding", "config", "error_match"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
4,
|
||||||
|
{"width": 320},
|
||||||
|
r"required key not provided @ data\['height'\]",
|
||||||
|
id="missing_height",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
32,
|
||||||
|
{"width": 320, "height": 111},
|
||||||
|
"Dimensions and offsets must be divisible by 32",
|
||||||
|
id="dimensions_not_divisible",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_dimension_validation(
|
||||||
|
rounding: int,
|
||||||
|
config: ConfigType,
|
||||||
|
error_match: str,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test dimension-related validation errors"""
|
||||||
|
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(cv.Invalid, match=error_match):
|
||||||
|
dimension_schema(rounding)(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("config", "error_match"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"model": "JC3248W535",
|
||||||
|
"transform": {"mirror_x": False, "mirror_y": True, "swap_xy": True},
|
||||||
|
},
|
||||||
|
"Axis swapping not supported by this model",
|
||||||
|
id="axis_swapping_not_supported",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"model": "custom",
|
||||||
|
"dimensions": {"width": 320, "height": 240},
|
||||||
|
"transform": {"mirror_x": False, "mirror_y": True, "swap_xy": False},
|
||||||
|
"init_sequence": [[0x36, 0x01]],
|
||||||
|
},
|
||||||
|
r"transform is not supported when MADCTL \(0X36\) is in the init sequence",
|
||||||
|
id="transform_with_madctl",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"model": "custom",
|
||||||
|
"dimensions": {"width": 320, "height": 240},
|
||||||
|
"init_sequence": [[0x3A, 0x01]],
|
||||||
|
},
|
||||||
|
r"PIXFMT \(0X3A\) should not be in the init sequence, it will be set automatically",
|
||||||
|
id="pixfmt_in_init_sequence",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_transform_and_init_sequence_errors(
|
||||||
|
config: ConfigType,
|
||||||
|
error_match: str,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test transform and init sequence validation errors"""
|
||||||
|
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(cv.Invalid, match=error_match):
|
||||||
|
run_schema_validation(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("config", "error_match"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
{"model": "t4-s3", "dc_pin": 18},
|
||||||
|
"DC pin is not supported in quad mode",
|
||||||
|
id="dc_pin_not_supported_quad_mode",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"model": "t4-s3", "color_depth": 18},
|
||||||
|
"Unknown value '18', valid options are '16', '16bit",
|
||||||
|
id="invalid_color_depth_t4_s3",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"model": "t-embed", "color_depth": 24},
|
||||||
|
"Unknown value '24', valid options are '16', '8",
|
||||||
|
id="invalid_color_depth_t_embed",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"model": "ili9488"},
|
||||||
|
"DC pin is required in single mode",
|
||||||
|
id="dc_pin_required_single_mode",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"model": "wt32-sc01-plus", "brightness": 128},
|
||||||
|
r"extra keys not allowed @ data\['brightness'\]",
|
||||||
|
id="brightness_not_supported",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"model": "T-DISPLAY-S3-PRO"},
|
||||||
|
"PSRAM is required for this display",
|
||||||
|
id="psram_required",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_esp32s3_specific_errors(
|
||||||
|
config: ConfigType,
|
||||||
|
error_match: str,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test ESP32-S3 specific configuration errors"""
|
||||||
|
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3},
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(cv.Invalid, match=error_match):
|
||||||
|
run_schema_validation(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_framework_specific_errors(
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test framework-specific configuration errors"""
|
||||||
|
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_ARDUINO,
|
||||||
|
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
cv.Invalid,
|
||||||
|
match=r"This feature is only available with frameworks \['esp-idf'\]",
|
||||||
|
):
|
||||||
|
run_schema_validation({"model": "wt32-sc01-plus"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_model_with_all_options(
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
) -> None:
|
||||||
|
"""Test custom model configuration with all available options."""
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3},
|
||||||
|
)
|
||||||
|
|
||||||
|
run_schema_validation(
|
||||||
|
{
|
||||||
|
"model": "custom",
|
||||||
|
"pixel_mode": "18bit",
|
||||||
|
"color_depth": 8,
|
||||||
|
"id": "display_id",
|
||||||
|
"byte_order": "little_endian",
|
||||||
|
"bus_mode": "single",
|
||||||
|
"color_order": "rgb",
|
||||||
|
"dc_pin": 11,
|
||||||
|
"reset_pin": 12,
|
||||||
|
"enable_pin": 13,
|
||||||
|
"cs_pin": 14,
|
||||||
|
"init_sequence": [[0xA0, 0x01]],
|
||||||
|
"dimensions": {
|
||||||
|
"width": 320,
|
||||||
|
"height": 240,
|
||||||
|
"offset_width": 32,
|
||||||
|
"offset_height": 32,
|
||||||
|
},
|
||||||
|
"invert_colors": True,
|
||||||
|
"transform": {"mirror_x": True, "mirror_y": True, "swap_xy": False},
|
||||||
|
"spi_mode": "mode0",
|
||||||
|
"data_rate": "40MHz",
|
||||||
|
"use_axis_flips": True,
|
||||||
|
"draw_rounding": 4,
|
||||||
|
"spi_16": True,
|
||||||
|
"buffer_size": 0.25,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_predefined_models(
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
|
set_component_config: Callable[[str, Any], None],
|
||||||
|
choose_variant_with_pins: Callable[..., None],
|
||||||
|
) -> None:
|
||||||
|
"""Test all predefined display models validate successfully with appropriate defaults."""
|
||||||
|
set_core_config(
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
|
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable PSRAM which is required for some models
|
||||||
|
set_component_config("psram", True)
|
||||||
|
|
||||||
|
# Test all models, providing default values where necessary
|
||||||
|
for name, model in MODELS.items():
|
||||||
|
config = {"model": name}
|
||||||
|
|
||||||
|
# Get the pins required by this model and find a compatible variant
|
||||||
|
pins = [
|
||||||
|
pin
|
||||||
|
for pin in [
|
||||||
|
model.get_default(pin, None)
|
||||||
|
for pin in ("dc_pin", "reset_pin", "cs_pin")
|
||||||
|
]
|
||||||
|
if pin is not None
|
||||||
|
]
|
||||||
|
choose_variant_with_pins(pins)
|
||||||
|
|
||||||
|
# Add required fields that don't have defaults
|
||||||
|
if (
|
||||||
|
not model.get_default(CONF_DC_PIN)
|
||||||
|
and model.get_default(CONF_BUS_MODE) != "quad"
|
||||||
|
):
|
||||||
|
config[CONF_DC_PIN] = 14
|
||||||
|
if not model.get_default(CONF_NATIVE_HEIGHT):
|
||||||
|
config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320}
|
||||||
|
if model.initsequence is None:
|
||||||
|
config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]]
|
||||||
|
|
||||||
|
run_schema_validation(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_generation(
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
component_fixture_path: Callable[[str], Path],
|
||||||
|
) -> None:
|
||||||
|
"""Test code generation for display."""
|
||||||
|
|
||||||
|
main_cpp = generate_main(component_fixture_path("native.yaml"))
|
||||||
|
assert (
|
||||||
|
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1>()"
|
||||||
|
in main_cpp
|
||||||
|
)
|
||||||
|
assert "set_init_sequence({240, 1, 8, 242" in main_cpp
|
||||||
|
assert "show_test_card();" in main_cpp
|
||||||
|
assert "set_write_only(true);" in main_cpp
|
||||||
|
|
||||||
|
|
||||||
|
def test_lvgl_generation(
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
component_fixture_path: Callable[[str], Path],
|
||||||
|
) -> None:
|
||||||
|
"""Test LVGL generation configuration."""
|
||||||
|
|
||||||
|
main_cpp = generate_main(component_fixture_path("lvgl.yaml"))
|
||||||
|
assert (
|
||||||
|
"mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0>();"
|
||||||
|
in main_cpp
|
||||||
|
)
|
||||||
|
assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp
|
||||||
|
assert "show_test_card();" not in main_cpp
|
||||||
|
assert "set_auto_clear(false);" in main_cpp
|
0
tests/component_tests/ota/__init__.py
Normal file
0
tests/component_tests/ota/__init__.py
Normal file
0
tests/component_tests/packages/__init__.py
Normal file
0
tests/component_tests/packages/__init__.py
Normal file
0
tests/component_tests/sensor/__init__.py
Normal file
0
tests/component_tests/sensor/__init__.py
Normal file
0
tests/component_tests/text/__init__.py
Normal file
0
tests/component_tests/text/__init__.py
Normal file
0
tests/component_tests/text_sensor/__init__.py
Normal file
0
tests/component_tests/text_sensor/__init__.py
Normal file
21
tests/component_tests/types.py
Normal file
21
tests/component_tests/types.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
"""Type definitions for component tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from esphome.const import PlatformFramework
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
|
|
||||||
|
class SetCoreConfigCallable(Protocol):
|
||||||
|
"""Protocol for the set_core_config fixture setter function."""
|
||||||
|
|
||||||
|
def __call__( # noqa: E704
|
||||||
|
self,
|
||||||
|
platform_framework: PlatformFramework,
|
||||||
|
/,
|
||||||
|
*,
|
||||||
|
core_data: ConfigType | None = None,
|
||||||
|
platform_data: ConfigType | None = None,
|
||||||
|
) -> None: ...
|
0
tests/component_tests/web_server/__init__.py
Normal file
0
tests/component_tests/web_server/__init__.py
Normal file
@ -1,41 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: quad_spi
|
|
||||||
type: quad
|
|
||||||
interface: spi3
|
|
||||||
clk_pin:
|
|
||||||
number: 47
|
|
||||||
data_pins:
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: octal_spi
|
|
||||||
type: octal
|
|
||||||
interface: hardware
|
|
||||||
clk_pin:
|
|
||||||
number: 0
|
|
||||||
data_pins:
|
|
||||||
- 36
|
|
||||||
- 37
|
|
||||||
- 38
|
|
||||||
- 39
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 9
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: ESP32-2432S028
|
|
@ -1,41 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: quad_spi
|
|
||||||
type: quad
|
|
||||||
interface: spi3
|
|
||||||
clk_pin:
|
|
||||||
number: 47
|
|
||||||
data_pins:
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: octal_spi
|
|
||||||
type: octal
|
|
||||||
interface: hardware
|
|
||||||
clk_pin:
|
|
||||||
number: 0
|
|
||||||
data_pins:
|
|
||||||
- 36
|
|
||||||
- 37
|
|
||||||
- 38
|
|
||||||
- 39
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 9
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: JC3248W535
|
|
@ -1,19 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: quad_spi
|
|
||||||
type: quad
|
|
||||||
interface: spi3
|
|
||||||
clk_pin:
|
|
||||||
number: 36
|
|
||||||
data_pins:
|
|
||||||
- number: 40
|
|
||||||
- number: 41
|
|
||||||
- number: 42
|
|
||||||
- number: 43
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 9
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: JC3636W518
|
|
18
tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml
Normal file
18
tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
substitutions:
|
||||||
|
clk_pin: GPIO16
|
||||||
|
mosi_pin: GPIO17
|
||||||
|
|
||||||
|
spi:
|
||||||
|
- id: spi_single
|
||||||
|
clk_pin:
|
||||||
|
number: ${clk_pin}
|
||||||
|
mosi_pin:
|
||||||
|
number: ${mosi_pin}
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: mipi_spi
|
||||||
|
model: t-display-s3-pro
|
||||||
|
|
||||||
|
lvgl:
|
||||||
|
|
||||||
|
psram:
|
@ -1,9 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 9
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: Pico-ResTouch-LCD-3.5
|
|
@ -1,41 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: quad_spi
|
|
||||||
type: quad
|
|
||||||
interface: spi3
|
|
||||||
clk_pin:
|
|
||||||
number: 47
|
|
||||||
data_pins:
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: octal_spi
|
|
||||||
type: octal
|
|
||||||
interface: hardware
|
|
||||||
clk_pin:
|
|
||||||
number: 0
|
|
||||||
data_pins:
|
|
||||||
- 36
|
|
||||||
- 37
|
|
||||||
- 38
|
|
||||||
- 39
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 9
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: S3BOX
|
|
@ -1,41 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: quad_spi
|
|
||||||
type: quad
|
|
||||||
interface: spi3
|
|
||||||
clk_pin:
|
|
||||||
number: 47
|
|
||||||
data_pins:
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: octal_spi
|
|
||||||
type: octal
|
|
||||||
interface: hardware
|
|
||||||
clk_pin:
|
|
||||||
number: 0
|
|
||||||
data_pins:
|
|
||||||
- 36
|
|
||||||
- 37
|
|
||||||
- 38
|
|
||||||
- 39
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 9
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: S3BOXLITE
|
|
@ -1,9 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 9
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: T-DISPLAY-S3-AMOLED-PLUS
|
|
@ -1,15 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: quad_spi
|
|
||||||
type: quad
|
|
||||||
interface: spi3
|
|
||||||
clk_pin:
|
|
||||||
number: 47
|
|
||||||
data_pins:
|
|
||||||
- number: 40
|
|
||||||
- number: 41
|
|
||||||
- number: 42
|
|
||||||
- number: 43
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: T-DISPLAY-S3-AMOLED
|
|
@ -1,9 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 40
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: T-DISPLAY-S3-PRO
|
|
@ -1,37 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: quad_spi
|
|
||||||
type: quad
|
|
||||||
interface: spi3
|
|
||||||
clk_pin:
|
|
||||||
number: 47
|
|
||||||
data_pins:
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: octal_spi
|
|
||||||
type: octal
|
|
||||||
interface: hardware
|
|
||||||
clk_pin:
|
|
||||||
number: 0
|
|
||||||
data_pins:
|
|
||||||
- 36
|
|
||||||
- 37
|
|
||||||
- 38
|
|
||||||
- 39
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: T-DISPLAY-S3
|
|
@ -1,41 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: quad_spi
|
|
||||||
type: quad
|
|
||||||
interface: spi3
|
|
||||||
clk_pin:
|
|
||||||
number: 47
|
|
||||||
data_pins:
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: octal_spi
|
|
||||||
type: octal
|
|
||||||
interface: hardware
|
|
||||||
clk_pin:
|
|
||||||
number: 0
|
|
||||||
data_pins:
|
|
||||||
- 36
|
|
||||||
- 37
|
|
||||||
- 38
|
|
||||||
- 39
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 9
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: T-DISPLAY
|
|
@ -1,9 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 40
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: T-EMBED
|
|
@ -1,41 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: quad_spi
|
|
||||||
type: quad
|
|
||||||
interface: spi3
|
|
||||||
clk_pin:
|
|
||||||
number: 47
|
|
||||||
data_pins:
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: octal_spi
|
|
||||||
type: octal
|
|
||||||
interface: hardware
|
|
||||||
clk_pin:
|
|
||||||
number: 0
|
|
||||||
data_pins:
|
|
||||||
- 36
|
|
||||||
- 37
|
|
||||||
- 38
|
|
||||||
- 39
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: spi_id_3
|
|
||||||
interface: any
|
|
||||||
clk_pin: 8
|
|
||||||
mosi_pin: 9
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: T4-S3
|
|
@ -1,37 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: quad_spi
|
|
||||||
type: quad
|
|
||||||
interface: spi3
|
|
||||||
clk_pin:
|
|
||||||
number: 47
|
|
||||||
data_pins:
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
- id: octal_spi
|
|
||||||
type: octal
|
|
||||||
interface: hardware
|
|
||||||
clk_pin:
|
|
||||||
number: 9
|
|
||||||
data_pins:
|
|
||||||
- 36
|
|
||||||
- 37
|
|
||||||
- 38
|
|
||||||
- 39
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 40
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 41
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 42
|
|
||||||
- allow_other_uses: true
|
|
||||||
number: 43
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: mipi_spi
|
|
||||||
model: WT32-SC01-PLUS
|
|
Loading…
x
Reference in New Issue
Block a user