mirror of
https://github.com/esphome/esphome.git
synced 2026-05-30 07:16:11 +08:00
Merge branch 'cron-trigger-interval' into integration
This commit is contained in:
@@ -235,19 +235,20 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get PR head to fetch files from the PR branch
|
// Get base branch ref to check if deprecation already exists for the component
|
||||||
const prNumber = context.payload.pull_request.number;
|
// This prevents flagging a PR that simply adds deprecation
|
||||||
|
const baseRef = context.payload.pull_request.base.ref;
|
||||||
|
|
||||||
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
|
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
|
||||||
for (const component of components) {
|
for (const component of components) {
|
||||||
const initFile = `esphome/components/${component}/__init__.py`;
|
const initFile = `esphome/components/${component}/__init__.py`;
|
||||||
try {
|
try {
|
||||||
// Fetch file content from PR head using GitHub API
|
// Fetch file content from base branch using GitHub API
|
||||||
const { data: fileData } = await github.rest.repos.getContent({
|
const { data: fileData } = await github.rest.repos.getContent({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
path: initFile,
|
path: initFile,
|
||||||
ref: `refs/pull/${prNumber}/head`
|
ref: baseRef
|
||||||
});
|
});
|
||||||
|
|
||||||
// Decode base64 content
|
// Decode base64 content
|
||||||
|
|||||||
@@ -723,7 +723,7 @@ jobs:
|
|||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
||||||
env:
|
env:
|
||||||
SKIP: pylint,clang-tidy-hash
|
SKIP: pylint,clang-tidy-hash,ci-custom
|
||||||
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
|
|||||||
@@ -65,3 +65,7 @@ repos:
|
|||||||
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
|
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
additional_dependencies: []
|
additional_dependencies: []
|
||||||
|
- id: ci-custom
|
||||||
|
name: ci-custom
|
||||||
|
entry: python3 script/run-in-env.py script/ci-custom.py
|
||||||
|
language: system
|
||||||
|
|||||||
@@ -32,4 +32,6 @@ ICON_CURRENT_DC = "mdi:current-dc"
|
|||||||
ICON_SOLAR_PANEL = "mdi:solar-panel"
|
ICON_SOLAR_PANEL = "mdi:solar-panel"
|
||||||
ICON_SOLAR_POWER = "mdi:solar-power"
|
ICON_SOLAR_POWER = "mdi:solar-power"
|
||||||
|
|
||||||
|
KEY_METADATA = "metadata"
|
||||||
|
|
||||||
UNIT_AMPERE_HOUR = "Ah"
|
UNIT_AMPERE_HOUR = "Ah"
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from esphome import automation, core
|
from esphome import automation, core
|
||||||
from esphome.automation import maybe_simple_id
|
from esphome.automation import maybe_simple_id
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.const import KEY_METADATA
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_AUTO_CLEAR_ENABLED,
|
CONF_AUTO_CLEAR_ENABLED,
|
||||||
@@ -16,7 +19,9 @@ from esphome.const import (
|
|||||||
SCHEDULER_DONT_RUN,
|
SCHEDULER_DONT_RUN,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||||
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
|
DOMAIN = "display"
|
||||||
IS_PLATFORM_COMPONENT = True
|
IS_PLATFORM_COMPONENT = True
|
||||||
|
|
||||||
display_ns = cg.esphome_ns.namespace("display")
|
display_ns = cg.esphome_ns.namespace("display")
|
||||||
@@ -146,6 +151,39 @@ async def setup_display_core_(var, config):
|
|||||||
cg.add(var.show_test_card())
|
cg.add(var.show_test_card())
|
||||||
|
|
||||||
|
|
||||||
|
# Storage of display metadata in a central location, accessible via the id
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DisplayMetaData:
|
||||||
|
width: int = 0
|
||||||
|
height: int = 0
|
||||||
|
has_writer: bool = False
|
||||||
|
has_hardware_rotation: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_display_metadata() -> dict[str, DisplayMetaData]:
|
||||||
|
"""Get all display metadata."""
|
||||||
|
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
|
||||||
|
|
||||||
|
|
||||||
|
def get_display_metadata(display_id: str) -> DisplayMetaData | None:
|
||||||
|
"""Get display metadata by ID for use by other components."""
|
||||||
|
return get_all_display_metadata().get(display_id, DisplayMetaData())
|
||||||
|
|
||||||
|
|
||||||
|
def add_metadata(
|
||||||
|
id: str | MockObj,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
has_writer: bool,
|
||||||
|
has_hardware_rotation: bool = False,
|
||||||
|
):
|
||||||
|
get_all_display_metadata()[str(id)] = DisplayMetaData(
|
||||||
|
width, height, has_writer, has_hardware_rotation
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def register_display(var, config):
|
async def register_display(var, config):
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await setup_display_core_(var, config)
|
await setup_display_core_(var, config)
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ class Display : public PollingComponent {
|
|||||||
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
|
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
|
||||||
|
|
||||||
/// Internal method to set the display rotation with.
|
/// Internal method to set the display rotation with.
|
||||||
void set_rotation(DisplayRotation rotation);
|
virtual void set_rotation(DisplayRotation rotation);
|
||||||
|
|
||||||
// Internal method to set display auto clearing.
|
// Internal method to set display auto clearing.
|
||||||
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
|
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, ar
|
|||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
|
|
||||||
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.double)
|
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.int_)
|
||||||
cg.add(var.set_address(template_))
|
cg.add(var.set_address(template_))
|
||||||
|
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -5,11 +5,17 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "preferences.h"
|
#include "preferences.h"
|
||||||
|
|
||||||
|
#include <csignal>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
volatile sig_atomic_t s_signal_received = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
void signal_handler(int signal) { s_signal_received = signal; }
|
||||||
|
} // namespace
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
void HOT yield() { ::sched_yield(); }
|
void HOT yield() { ::sched_yield(); }
|
||||||
@@ -72,11 +78,17 @@ uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
|
|||||||
void setup();
|
void setup();
|
||||||
void loop();
|
void loop();
|
||||||
int main() {
|
int main() {
|
||||||
|
// Install signal handlers for graceful shutdown (flushes preferences to disk)
|
||||||
|
std::signal(SIGINT, signal_handler);
|
||||||
|
std::signal(SIGTERM, signal_handler);
|
||||||
|
|
||||||
esphome::host::setup_preferences();
|
esphome::host::setup_preferences();
|
||||||
setup();
|
setup();
|
||||||
while (true) {
|
while (s_signal_received == 0) {
|
||||||
loop();
|
loop();
|
||||||
}
|
}
|
||||||
|
esphome::App.run_safe_shutdown_hooks();
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // USE_HOST
|
#endif // USE_HOST
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
DEPRECATED_COMPONENT = """
|
||||||
|
The 'ili9xxx' component is deprecated and no new models will be added to it.
|
||||||
|
New model PRs should target the newer and more performant 'mipi_spi' component.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -210,8 +210,8 @@ def final_validate(config):
|
|||||||
):
|
):
|
||||||
LOGGER.info("Consider enabling PSRAM if available for the display buffer")
|
LOGGER.info("Consider enabling PSRAM if available for the display buffer")
|
||||||
|
|
||||||
return spi.final_validate_device_schema(
|
spi.final_validate_device_schema("ili9xxx", require_miso=False, require_mosi=True)(
|
||||||
"ili9xxx", require_miso=False, require_mosi=True
|
config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -219,6 +219,9 @@ FINAL_VALIDATE_SCHEMA = final_validate
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
|
LOGGER.warning(
|
||||||
|
"The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead."
|
||||||
|
)
|
||||||
rhs = MODELS[config[CONF_MODEL]].new()
|
rhs = MODELS[config[CONF_MODEL]].new()
|
||||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from PIL import Image, UnidentifiedImageError
|
|||||||
|
|
||||||
from esphome import core, external_files
|
from esphome import core, external_files
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.const import CONF_BYTE_ORDER
|
from esphome.components.const import CONF_BYTE_ORDER, KEY_METADATA
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_DEFAULTS,
|
CONF_DEFAULTS,
|
||||||
@@ -53,7 +53,6 @@ CONF_CHROMA_KEY = "chroma_key"
|
|||||||
CONF_ALPHA_CHANNEL = "alpha_channel"
|
CONF_ALPHA_CHANNEL = "alpha_channel"
|
||||||
CONF_INVERT_ALPHA = "invert_alpha"
|
CONF_INVERT_ALPHA = "invert_alpha"
|
||||||
CONF_IMAGES = "images"
|
CONF_IMAGES = "images"
|
||||||
KEY_METADATA = "metadata"
|
|
||||||
|
|
||||||
TRANSPARENCY_TYPES = (
|
TRANSPARENCY_TYPES = (
|
||||||
CONF_OPAQUE,
|
CONF_OPAQUE,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from esphome.core.config import StartupTrigger
|
|||||||
|
|
||||||
from . import defines as df, lv_validation as lvalid
|
from . import defines as df, lv_validation as lvalid
|
||||||
from .defines import (
|
from .defines import (
|
||||||
|
CONF_EXT_CLICK_AREA,
|
||||||
CONF_SCROLL_DIR,
|
CONF_SCROLL_DIR,
|
||||||
CONF_SCROLL_SNAP_X,
|
CONF_SCROLL_SNAP_X,
|
||||||
CONF_SCROLL_SNAP_Y,
|
CONF_SCROLL_SNAP_Y,
|
||||||
@@ -311,6 +312,7 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex
|
|||||||
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
|
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
|
||||||
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
|
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
|
||||||
).one_of,
|
).one_of,
|
||||||
|
cv.Optional(CONF_EXT_CLICK_AREA): lvalid.pixels,
|
||||||
cv.Optional(CONF_SCROLL_DIR): df.SCROLL_DIRECTIONS.one_of,
|
cv.Optional(CONF_SCROLL_DIR): df.SCROLL_DIRECTIONS.one_of,
|
||||||
cv.Optional(CONF_SCROLL_SNAP_X): df.SNAP_DIRECTIONS.one_of,
|
cv.Optional(CONF_SCROLL_SNAP_X): df.SNAP_DIRECTIONS.one_of,
|
||||||
cv.Optional(CONF_SCROLL_SNAP_Y): df.SNAP_DIRECTIONS.one_of,
|
cv.Optional(CONF_SCROLL_SNAP_Y): df.SNAP_DIRECTIONS.one_of,
|
||||||
@@ -318,6 +320,7 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex
|
|||||||
)
|
)
|
||||||
|
|
||||||
OBJ_PROPERTIES = {
|
OBJ_PROPERTIES = {
|
||||||
|
CONF_EXT_CLICK_AREA,
|
||||||
CONF_SCROLL_SNAP_X,
|
CONF_SCROLL_SNAP_X,
|
||||||
CONF_SCROLL_SNAP_Y,
|
CONF_SCROLL_SNAP_Y,
|
||||||
CONF_SCROLL_DIR,
|
CONF_SCROLL_DIR,
|
||||||
@@ -433,7 +436,6 @@ def obj_schema(widget_type: WidgetType):
|
|||||||
return (
|
return (
|
||||||
part_schema(widget_type.parts)
|
part_schema(widget_type.parts)
|
||||||
.extend(ALIGN_TO_SCHEMA)
|
.extend(ALIGN_TO_SCHEMA)
|
||||||
.extend({cv.Optional(df.CONF_EXT_CLICK_AREA): lvalid.pixels})
|
|
||||||
.extend(automation_schema(widget_type.w_type))
|
.extend(automation_schema(widget_type.w_type))
|
||||||
.extend(
|
.extend(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from .defines import (
|
|||||||
CONF_ALIGN,
|
CONF_ALIGN,
|
||||||
CONF_ALIGN_TO,
|
CONF_ALIGN_TO,
|
||||||
CONF_ALIGN_TO_LAMBDA_ID,
|
CONF_ALIGN_TO_LAMBDA_ID,
|
||||||
CONF_EXT_CLICK_AREA,
|
|
||||||
DIRECTIONS,
|
DIRECTIONS,
|
||||||
LV_EVENT_MAP,
|
LV_EVENT_MAP,
|
||||||
LV_EVENT_TRIGGERS,
|
LV_EVENT_TRIGGERS,
|
||||||
@@ -114,8 +113,6 @@ async def generate_align_tos(config: dict):
|
|||||||
x = align_to[CONF_X]
|
x = align_to[CONF_X]
|
||||||
y = align_to[CONF_Y]
|
y = align_to[CONF_Y]
|
||||||
lv.obj_align_to(w.obj, target, align, x, y)
|
lv.obj_align_to(w.obj, target, align, x, y)
|
||||||
if ext_click_area := w.config.get(CONF_EXT_CLICK_AREA):
|
|
||||||
lv.obj_set_ext_click_area(w.obj, ext_click_area)
|
|
||||||
|
|
||||||
action_id = config[CONF_ALIGN_TO_LAMBDA_ID]
|
action_id = config[CONF_ALIGN_TO_LAMBDA_ID]
|
||||||
var = new_Pvariable(action_id, await context.get_lambda())
|
var = new_Pvariable(action_id, await context.get_lambda())
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left
|
|||||||
MADCTL_XFLIP = 0x02 # Mirror the display horizontally
|
MADCTL_XFLIP = 0x02 # Mirror the display horizontally
|
||||||
MADCTL_YFLIP = 0x01 # Mirror the display vertically
|
MADCTL_YFLIP = 0x01 # Mirror the display vertically
|
||||||
|
|
||||||
|
MADCTL_FLIP_FLAG = 0x100 # meta-flag to indicate use of axis flips
|
||||||
|
|
||||||
# Special constant for delays in command sequences
|
# Special constant for delays in command sequences
|
||||||
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
|
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
|
||||||
|
|
||||||
@@ -329,7 +331,13 @@ class DriverChip:
|
|||||||
return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms
|
return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms
|
||||||
return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms
|
return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms
|
||||||
|
|
||||||
def get_dimensions(self, config) -> tuple[int, int, int, int]:
|
def get_dimensions(self, config, swap: bool = True) -> tuple[int, int, int, int]:
|
||||||
|
"""
|
||||||
|
Return the dimensions of the current model.
|
||||||
|
:param config: The current configuration
|
||||||
|
:param swap: If width/height should be swapped when axes are swapped.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
if CONF_DIMENSIONS in config:
|
if CONF_DIMENSIONS in config:
|
||||||
# Explicit dimensions, just use as is
|
# Explicit dimensions, just use as is
|
||||||
dimensions = config[CONF_DIMENSIONS]
|
dimensions = config[CONF_DIMENSIONS]
|
||||||
@@ -361,13 +369,12 @@ class DriverChip:
|
|||||||
)
|
)
|
||||||
offset_height = native_height - height - offset_height
|
offset_height = native_height - height - offset_height
|
||||||
# Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer
|
# Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer
|
||||||
if transform.get(CONF_SWAP_XY) is True:
|
if swap and transform.get(CONF_SWAP_XY) is True:
|
||||||
width, height = height, width
|
width, height = height, width
|
||||||
offset_height, offset_width = offset_width, offset_height
|
offset_height, offset_width = offset_width, offset_height
|
||||||
return width, height, offset_width, offset_height
|
return width, height, offset_width, offset_height
|
||||||
|
|
||||||
def get_transform(self, config) -> dict[str, bool]:
|
def get_base_transform(self, config):
|
||||||
can_transform = self.rotation_as_transform(config)
|
|
||||||
transform = config.get(
|
transform = config.get(
|
||||||
CONF_TRANSFORM,
|
CONF_TRANSFORM,
|
||||||
{
|
{
|
||||||
@@ -376,14 +383,20 @@ class DriverChip:
|
|||||||
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
|
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if not isinstance(transform, dict):
|
if isinstance(transform, dict):
|
||||||
# Presumably disabled
|
return transform
|
||||||
return {
|
|
||||||
CONF_MIRROR_X: False,
|
# Transform is disabled
|
||||||
CONF_MIRROR_Y: False,
|
return {
|
||||||
CONF_SWAP_XY: False,
|
CONF_MIRROR_X: False,
|
||||||
CONF_TRANSFORM: False,
|
CONF_MIRROR_Y: False,
|
||||||
}
|
CONF_SWAP_XY: False,
|
||||||
|
CONF_TRANSFORM: False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_transform(self, config) -> dict[str, bool]:
|
||||||
|
transform = self.get_base_transform(config)
|
||||||
|
can_transform = self.rotation_as_transform(config)
|
||||||
# Can we use the MADCTL register to set the rotation?
|
# Can we use the MADCTL register to set the rotation?
|
||||||
if can_transform and CONF_TRANSFORM not in config:
|
if can_transform and CONF_TRANSFORM not in config:
|
||||||
rotation = config[CONF_ROTATION]
|
rotation = config[CONF_ROTATION]
|
||||||
@@ -411,11 +424,15 @@ class DriverChip:
|
|||||||
return {cv.Required(CONF_SWAP_XY): cv.boolean}
|
return {cv.Required(CONF_SWAP_XY): cv.boolean}
|
||||||
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
|
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
|
||||||
|
|
||||||
def add_madctl(self, sequence: list, config: dict):
|
def get_madctl(self, transform: dict, config: dict) -> int:
|
||||||
# Add the MADCTL command to the sequence based on the configuration.
|
"""
|
||||||
use_flip = config.get(CONF_USE_AXIS_FLIPS)
|
Convert a transform to MADCTL bits
|
||||||
madctl = 0
|
:param transform: The transform dict
|
||||||
transform = self.get_transform(config)
|
:param use_flip: Whether to use axis flips
|
||||||
|
:return: MADCTL value
|
||||||
|
"""
|
||||||
|
use_flip = config.get(CONF_USE_AXIS_FLIPS, False)
|
||||||
|
madctl = MADCTL_FLIP_FLAG if use_flip else 0
|
||||||
if transform[CONF_MIRROR_X]:
|
if transform[CONF_MIRROR_X]:
|
||||||
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
|
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
|
||||||
if transform[CONF_MIRROR_Y]:
|
if transform[CONF_MIRROR_Y]:
|
||||||
@@ -424,22 +441,28 @@ class DriverChip:
|
|||||||
madctl |= MADCTL_MV
|
madctl |= MADCTL_MV
|
||||||
if config[CONF_COLOR_ORDER] == MODE_BGR:
|
if config[CONF_COLOR_ORDER] == MODE_BGR:
|
||||||
madctl |= MADCTL_BGR
|
madctl |= MADCTL_BGR
|
||||||
sequence.append((MADCTL, madctl))
|
|
||||||
return madctl
|
return madctl
|
||||||
|
|
||||||
|
def add_madctl(self, sequence: list, config: dict):
|
||||||
|
# Add the MADCTL command to the sequence based on the configuration.
|
||||||
|
# This takes into account rotation if it can be implemented in the transform
|
||||||
|
transform = self.get_transform(config)
|
||||||
|
madctl = self.get_madctl(transform, config)
|
||||||
|
sequence.append((MADCTL, madctl & 0xFF))
|
||||||
|
|
||||||
def skip_command(self, command: str):
|
def skip_command(self, command: str):
|
||||||
"""
|
"""
|
||||||
Allow suppressing a standard command in the init sequence.
|
Allow suppressing a standard command in the init sequence.
|
||||||
"""
|
"""
|
||||||
return self.get_default(f"no_{command.lower()}", False)
|
return self.get_default(f"no_{command.lower()}", False)
|
||||||
|
|
||||||
def get_sequence(self, config) -> tuple[tuple[int, ...], int]:
|
def get_sequence(self, config, add_madctl=True) -> tuple[int, ...]:
|
||||||
"""
|
"""
|
||||||
Create the init sequence for the display.
|
Create the init sequence for the display.
|
||||||
Use the default sequence from the model, if any, and append any custom sequence provided in the config.
|
Use the default sequence from the model, if any, and append any custom sequence provided in the config.
|
||||||
Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence
|
Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence
|
||||||
Pixel format, color order, and orientation will be set.
|
MADCTL will be set if add_madctl is True
|
||||||
Returns a tuple of the init sequence and the computed MADCTL value.
|
Returns the init sequence
|
||||||
"""
|
"""
|
||||||
sequence = list(self.initsequence or ())
|
sequence = list(self.initsequence or ())
|
||||||
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
|
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
|
||||||
@@ -457,7 +480,8 @@ class DriverChip:
|
|||||||
|
|
||||||
if self.rotation_as_transform(config):
|
if self.rotation_as_transform(config):
|
||||||
LOGGER.info("Using hardware transform to implement rotation")
|
LOGGER.info("Using hardware transform to implement rotation")
|
||||||
madctl = self.add_madctl(sequence, config)
|
if add_madctl:
|
||||||
|
self.add_madctl(sequence, config)
|
||||||
if config[CONF_INVERT_COLORS]:
|
if config[CONF_INVERT_COLORS]:
|
||||||
sequence.append((INVON,))
|
sequence.append((INVON,))
|
||||||
else:
|
else:
|
||||||
@@ -471,7 +495,7 @@ class DriverChip:
|
|||||||
|
|
||||||
# Flatten the sequence into a list of bytes, with the length of each command
|
# Flatten the sequence into a list of bytes, with the length of each command
|
||||||
# or the delay flag inserted where needed
|
# or the delay flag inserted where needed
|
||||||
return flatten_sequence(sequence), madctl
|
return flatten_sequence(sequence)
|
||||||
|
|
||||||
|
|
||||||
def requires_buffer(config) -> bool:
|
def requires_buffer(config) -> bool:
|
||||||
|
|||||||
@@ -192,10 +192,9 @@ async def to_code(config):
|
|||||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||||
var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode)
|
var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode)
|
||||||
|
|
||||||
sequence, madctl = model.get_sequence(config)
|
sequence = model.get_sequence(config)
|
||||||
cg.add(var.set_model(config[CONF_MODEL]))
|
cg.add(var.set_model(config[CONF_MODEL]))
|
||||||
cg.add(var.set_init_sequence(sequence))
|
cg.add(var.set_init_sequence(sequence))
|
||||||
cg.add(var.set_madctl(madctl))
|
|
||||||
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
|
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
|
||||||
cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH]))
|
cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH]))
|
||||||
cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH]))
|
cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH]))
|
||||||
|
|||||||
@@ -392,9 +392,6 @@ void MIPI_DSI::dump_config() {
|
|||||||
"\n Model: %s"
|
"\n Model: %s"
|
||||||
"\n Width: %u"
|
"\n Width: %u"
|
||||||
"\n Height: %u"
|
"\n Height: %u"
|
||||||
"\n Mirror X: %s"
|
|
||||||
"\n Mirror Y: %s"
|
|
||||||
"\n Swap X/Y: %s"
|
|
||||||
"\n Rotation: %d degrees"
|
"\n Rotation: %d degrees"
|
||||||
"\n DSI Lanes: %u"
|
"\n DSI Lanes: %u"
|
||||||
"\n Lane Bit Rate: %.0fMbps"
|
"\n Lane Bit Rate: %.0fMbps"
|
||||||
@@ -406,14 +403,11 @@ void MIPI_DSI::dump_config() {
|
|||||||
"\n VSync Front Porch: %u"
|
"\n VSync Front Porch: %u"
|
||||||
"\n Buffer Color Depth: %d bit"
|
"\n Buffer Color Depth: %d bit"
|
||||||
"\n Display Pixel Mode: %d bit"
|
"\n Display Pixel Mode: %d bit"
|
||||||
"\n Color Order: %s"
|
|
||||||
"\n Invert Colors: %s"
|
"\n Invert Colors: %s"
|
||||||
"\n Pixel Clock: %.1fMHz",
|
"\n Pixel Clock: %.1fMHz",
|
||||||
this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)),
|
this->model_, this->width_, this->height_, this->rotation_, this->lanes_, this->lane_bit_rate_,
|
||||||
YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_,
|
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
|
||||||
this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_,
|
this->vsync_back_porch_, this->vsync_front_porch_, (3 - this->color_depth_) * 8, this->pixel_mode_,
|
||||||
this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, this->vsync_front_porch_,
|
|
||||||
(3 - this->color_depth_) * 8, this->pixel_mode_, this->madctl_ & MADCTL_BGR ? "BGR" : "RGB",
|
|
||||||
YESNO(this->invert_colors_), this->pclk_frequency_);
|
YESNO(this->invert_colors_), this->pclk_frequency_);
|
||||||
LOG_PIN(" Reset Pin ", this->reset_pin_);
|
LOG_PIN(" Reset Pin ", this->reset_pin_);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ class MIPI_DSI : public display::Display {
|
|||||||
void set_model(const char *model) { this->model_ = model; }
|
void set_model(const char *model) { this->model_ = model; }
|
||||||
void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; }
|
void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; }
|
||||||
void set_lanes(uint8_t lanes) { this->lanes_ = lanes; }
|
void set_lanes(uint8_t lanes) { this->lanes_ = lanes; }
|
||||||
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
|
|
||||||
|
|
||||||
void smark_failed(const LogString *message, esp_err_t err);
|
void smark_failed(const LogString *message, esp_err_t err);
|
||||||
|
|
||||||
@@ -86,7 +85,6 @@ class MIPI_DSI : public display::Display {
|
|||||||
std::vector<GPIOPin *> enable_pins_{};
|
std::vector<GPIOPin *> enable_pins_{};
|
||||||
size_t width_{};
|
size_t width_{};
|
||||||
size_t height_{};
|
size_t height_{};
|
||||||
uint8_t madctl_{};
|
|
||||||
uint16_t hsync_pulse_width_ = 10;
|
uint16_t hsync_pulse_width_ = 10;
|
||||||
uint16_t hsync_back_porch_ = 10;
|
uint16_t hsync_back_porch_ = 10;
|
||||||
uint16_t hsync_front_porch_ = 20;
|
uint16_t hsync_front_porch_ = 20;
|
||||||
|
|||||||
@@ -265,9 +265,8 @@ async def to_code(config):
|
|||||||
|
|
||||||
if CONF_SPI_ID in config:
|
if CONF_SPI_ID in config:
|
||||||
await spi.register_spi_device(var, config, write_only=True)
|
await spi.register_spi_device(var, config, write_only=True)
|
||||||
sequence, madctl = model.get_sequence(config)
|
sequence = model.get_sequence(config)
|
||||||
cg.add(var.set_init_sequence(sequence))
|
cg.add(var.set_init_sequence(sequence))
|
||||||
cg.add(var.set_madctl(madctl))
|
|
||||||
|
|
||||||
cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
|
cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
|
||||||
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
|
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
|
||||||
|
|||||||
@@ -118,15 +118,7 @@ void MipiRgbSpi::dump_config() {
|
|||||||
MipiRgb::dump_config();
|
MipiRgb::dump_config();
|
||||||
LOG_PIN(" CS Pin: ", this->cs_);
|
LOG_PIN(" CS Pin: ", this->cs_);
|
||||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG, " SPI Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000));
|
||||||
" SPI Data rate: %uMHz"
|
|
||||||
"\n Mirror X: %s"
|
|
||||||
"\n Mirror Y: %s"
|
|
||||||
"\n Swap X/Y: %s"
|
|
||||||
"\n Color Order: %s",
|
|
||||||
(unsigned) (this->data_rate_ / 1000000), YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)),
|
|
||||||
YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY | MADCTL_ML)), YESNO(this->madctl_ & MADCTL_MV),
|
|
||||||
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // USE_SPI
|
#endif // USE_SPI
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class MipiRgb : public display::Display {
|
|||||||
display::ColorOrder get_color_mode() { return this->color_mode_; }
|
display::ColorOrder get_color_mode() { return this->color_mode_; }
|
||||||
void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; }
|
void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; }
|
||||||
void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
|
void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
|
||||||
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
|
|
||||||
|
|
||||||
void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; };
|
void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; };
|
||||||
void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; }
|
void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; }
|
||||||
@@ -84,7 +83,6 @@ class MipiRgb : public display::Display {
|
|||||||
uint16_t vsync_front_porch_ = 10;
|
uint16_t vsync_front_porch_ = 10;
|
||||||
uint32_t pclk_frequency_ = 16 * 1000 * 1000;
|
uint32_t pclk_frequency_ = 16 * 1000 * 1000;
|
||||||
bool pclk_inverted_{true};
|
bool pclk_inverted_{true};
|
||||||
uint8_t madctl_{};
|
|
||||||
const char *model_{"Unknown"};
|
const char *model_{"Unknown"};
|
||||||
bool invert_colors_{};
|
bool invert_colors_{};
|
||||||
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
|
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from esphome.components.const import (
|
|||||||
CONF_COLOR_DEPTH,
|
CONF_COLOR_DEPTH,
|
||||||
CONF_DRAW_ROUNDING,
|
CONF_DRAW_ROUNDING,
|
||||||
)
|
)
|
||||||
from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS
|
from esphome.components.display import CONF_SHOW_TEST_CARD
|
||||||
from esphome.components.mipi import (
|
from esphome.components.mipi import (
|
||||||
CONF_PIXEL_MODE,
|
CONF_PIXEL_MODE,
|
||||||
CONF_USE_AXIS_FLIPS,
|
CONF_USE_AXIS_FLIPS,
|
||||||
@@ -47,12 +47,10 @@ from esphome.const import (
|
|||||||
CONF_MIRROR_Y,
|
CONF_MIRROR_Y,
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
CONF_RESET_PIN,
|
CONF_RESET_PIN,
|
||||||
CONF_ROTATION,
|
|
||||||
CONF_SWAP_XY,
|
CONF_SWAP_XY,
|
||||||
CONF_TRANSFORM,
|
CONF_TRANSFORM,
|
||||||
CONF_WIDTH,
|
CONF_WIDTH,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
|
||||||
from esphome.cpp_generator import TemplateArguments
|
from esphome.cpp_generator import TemplateArguments
|
||||||
from esphome.final_validate import full_config
|
from esphome.final_validate import full_config
|
||||||
|
|
||||||
@@ -113,22 +111,21 @@ DISPLAY_PIXEL_MODES = {
|
|||||||
def denominator(config):
|
def denominator(config):
|
||||||
"""
|
"""
|
||||||
Calculate the best denominator for a buffer size fraction.
|
Calculate the best denominator for a buffer size fraction.
|
||||||
The denominator must be a number between 2 and 16 that divides the display height evenly,
|
The denominator should be a number between 2 and 16 that divides the display height evenly,
|
||||||
and the fraction represented by the denominator must be less than or equal to the given fraction.
|
and the fraction represented by the denominator must be less than or equal to the given fraction.
|
||||||
:config: The configuration dictionary containing the buffer size fraction and display dimensions
|
:config: The configuration dictionary containing the buffer size fraction and display dimensions
|
||||||
:return: The denominator to use for the buffer size fraction
|
:return: The denominator to use for the buffer size fraction
|
||||||
"""
|
"""
|
||||||
model = MODELS[config[CONF_MODEL]]
|
model = MODELS[config[CONF_MODEL]]
|
||||||
frac = config.get(CONF_BUFFER_SIZE)
|
frac = config.get(CONF_BUFFER_SIZE)
|
||||||
if frac is None or frac > 0.75:
|
_width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||||
|
if frac is None or frac > 0.75 or height < 32:
|
||||||
return 1
|
return 1
|
||||||
height, _width, _offset_width, _offset_height = model.get_dimensions(config)
|
|
||||||
try:
|
try:
|
||||||
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
|
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise cv.Invalid(
|
# No exact divisor, just use the closest.
|
||||||
f"Buffer size fraction {frac} is not compatible with display height {height}"
|
return next(x for x in range(2, 17) if frac >= 1 / x)
|
||||||
) from StopIteration
|
|
||||||
|
|
||||||
|
|
||||||
def model_schema(config):
|
def model_schema(config):
|
||||||
@@ -287,30 +284,19 @@ def _final_validate(config):
|
|||||||
config[CONF_SHOW_TEST_CARD] = True
|
config[CONF_SHOW_TEST_CARD] = True
|
||||||
|
|
||||||
if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
|
if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
|
||||||
if not requires_buffer(config):
|
|
||||||
return config # No buffer needed, so no need to set a buffer size
|
|
||||||
# If PSRAM is not enabled, choose a small buffer size by default
|
# If PSRAM is not enabled, choose a small buffer size by default
|
||||||
if not requires_buffer(config):
|
if not requires_buffer(config):
|
||||||
# not our problem.
|
return config # No buffer needed, so no need to set a buffer size
|
||||||
return config
|
|
||||||
color_depth = get_color_depth(config)
|
color_depth = get_color_depth(config)
|
||||||
frac = denominator(config)
|
frac = denominator(config)
|
||||||
height, width, _offset_width, _offset_height = model.get_dimensions(config)
|
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||||
|
|
||||||
buffer_size = color_depth // 8 * width * height // frac
|
buffer_size = color_depth // 8 * width * height // frac
|
||||||
# Target a buffer size of 20kB
|
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
|
||||||
fraction = 20000.0 / buffer_size
|
fraction = min(20000.0, buffer_size // 16) / buffer_size
|
||||||
try:
|
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
||||||
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
x for x in range(2, 17) if fraction >= 1 / x
|
||||||
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
|
return config
|
||||||
|
|
||||||
@@ -318,39 +304,6 @@ def _final_validate(config):
|
|||||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|
||||||
|
|
||||||
def get_transform(config):
|
|
||||||
"""
|
|
||||||
Get the transformation configuration for the display.
|
|
||||||
:param config:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
model = MODELS[config[CONF_MODEL]]
|
|
||||||
can_transform = model.rotation_as_transform(config)
|
|
||||||
transform = config.get(
|
|
||||||
CONF_TRANSFORM,
|
|
||||||
{
|
|
||||||
CONF_MIRROR_X: model.get_default(CONF_MIRROR_X, False),
|
|
||||||
CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y, False),
|
|
||||||
CONF_SWAP_XY: model.get_default(CONF_SWAP_XY, False),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Can we use the MADCTL register to set the rotation?
|
|
||||||
if can_transform and CONF_TRANSFORM not in config:
|
|
||||||
rotation = config[CONF_ROTATION]
|
|
||||||
if rotation == 180:
|
|
||||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
|
||||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
|
||||||
elif rotation == 90:
|
|
||||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
|
||||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
|
||||||
else:
|
|
||||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
|
||||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
|
||||||
transform[CONF_TRANSFORM] = True
|
|
||||||
return transform
|
|
||||||
|
|
||||||
|
|
||||||
def get_instance(config):
|
def get_instance(config):
|
||||||
"""
|
"""
|
||||||
Get the type of MipiSpi instance to create based on the configuration,
|
Get the type of MipiSpi instance to create based on the configuration,
|
||||||
@@ -359,7 +312,16 @@ def get_instance(config):
|
|||||||
:return: type, template arguments
|
:return: type, template arguments
|
||||||
"""
|
"""
|
||||||
model = MODELS[config[CONF_MODEL]]
|
model = MODELS[config[CONF_MODEL]]
|
||||||
width, height, offset_width, offset_height = model.get_dimensions(config)
|
has_hardware_transform = config.get(
|
||||||
|
CONF_TRANSFORM
|
||||||
|
) != CONF_DISABLED and model.transforms == {
|
||||||
|
CONF_MIRROR_X,
|
||||||
|
CONF_MIRROR_Y,
|
||||||
|
CONF_SWAP_XY,
|
||||||
|
}
|
||||||
|
width, height, offset_width, offset_height = model.get_dimensions(
|
||||||
|
config, not has_hardware_transform
|
||||||
|
)
|
||||||
|
|
||||||
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
|
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
|
||||||
bufferpixels = COLOR_DEPTHS[color_depth]
|
bufferpixels = COLOR_DEPTHS[color_depth]
|
||||||
@@ -373,57 +335,43 @@ def get_instance(config):
|
|||||||
bus_type = BusTypes[bus_type]
|
bus_type = BusTypes[bus_type]
|
||||||
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
|
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
|
||||||
frac = denominator(config)
|
frac = denominator(config)
|
||||||
rotation = (
|
madctl = model.get_madctl(model.get_base_transform(config), config)
|
||||||
0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0)
|
has_writer = requires_buffer(config)
|
||||||
)
|
|
||||||
templateargs = [
|
templateargs = [
|
||||||
buffer_type,
|
buffer_type,
|
||||||
bufferpixels,
|
bufferpixels,
|
||||||
config[CONF_BYTE_ORDER] == "big_endian",
|
config[CONF_BYTE_ORDER] == "big_endian",
|
||||||
display_pixel_mode,
|
display_pixel_mode,
|
||||||
bus_type,
|
bus_type,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
offset_width,
|
||||||
|
offset_height,
|
||||||
|
madctl,
|
||||||
|
has_hardware_transform,
|
||||||
]
|
]
|
||||||
|
display.add_metadata(
|
||||||
|
config[CONF_ID], width, height, has_writer, has_hardware_transform
|
||||||
|
)
|
||||||
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
|
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
|
||||||
if requires_buffer(config):
|
if requires_buffer(config):
|
||||||
templateargs.extend(
|
templateargs.extend(
|
||||||
[
|
[
|
||||||
width,
|
|
||||||
height,
|
|
||||||
offset_width,
|
|
||||||
offset_height,
|
|
||||||
DISPLAY_ROTATIONS[rotation],
|
|
||||||
frac,
|
frac,
|
||||||
config[CONF_DRAW_ROUNDING],
|
config[CONF_DRAW_ROUNDING],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return MipiSpiBuffer, templateargs
|
return MipiSpiBuffer, templateargs
|
||||||
# Swap height and width if the display is rotated 90 or 270 degrees in software
|
|
||||||
if rotation in (90, 270):
|
|
||||||
width, height = height, width
|
|
||||||
offset_width, offset_height = offset_height, offset_width
|
|
||||||
templateargs.extend(
|
|
||||||
[
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
offset_width,
|
|
||||||
offset_height,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return MipiSpi, templateargs
|
return MipiSpi, templateargs
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
model = MODELS[config[CONF_MODEL]]
|
model = MODELS[config[CONF_MODEL]]
|
||||||
var_id = config[CONF_ID]
|
var_id = config[CONF_ID]
|
||||||
|
init_sequence = model.get_sequence(config, False)
|
||||||
var_id.type, templateargs = get_instance(config)
|
var_id.type, templateargs = get_instance(config)
|
||||||
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
|
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
|
||||||
init_sequence, _madctl = model.get_sequence(config)
|
|
||||||
cg.add(var.set_init_sequence(init_sequence))
|
cg.add(var.set_init_sequence(init_sequence))
|
||||||
if model.rotation_as_transform(config):
|
|
||||||
if CONF_TRANSFORM in config:
|
|
||||||
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
|
|
||||||
else:
|
|
||||||
config[CONF_ROTATION] = 0
|
|
||||||
cg.add(var.set_model(config[CONF_MODEL]))
|
cg.add(var.set_model(config[CONF_MODEL]))
|
||||||
if enable_pin := config.get(CONF_ENABLE_PIN):
|
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||||
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
|
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ namespace esphome::mipi_spi {
|
|||||||
|
|
||||||
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
|
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
|
||||||
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
|
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
|
||||||
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) {
|
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width,
|
||||||
|
bool has_hardware_rotation) {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
"MIPI_SPI Display\n"
|
"MIPI_SPI Display\n"
|
||||||
" Model: %s\n"
|
" Model: %s\n"
|
||||||
@@ -14,6 +15,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
|
|||||||
" Swap X/Y: %s\n"
|
" Swap X/Y: %s\n"
|
||||||
" Mirror X: %s\n"
|
" Mirror X: %s\n"
|
||||||
" Mirror Y: %s\n"
|
" Mirror Y: %s\n"
|
||||||
|
" Hardware rotation: %s\n"
|
||||||
" Invert colors: %s\n"
|
" Invert colors: %s\n"
|
||||||
" Color order: %s\n"
|
" Color order: %s\n"
|
||||||
" Display pixels: %d bits\n"
|
" Display pixels: %d bits\n"
|
||||||
@@ -22,9 +24,9 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
|
|||||||
" SPI Data rate: %uMHz\n"
|
" SPI Data rate: %uMHz\n"
|
||||||
" SPI Bus width: %d",
|
" SPI Bus width: %d",
|
||||||
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
|
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
|
||||||
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB",
|
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(has_hardware_rotation), YESNO(invert_colors),
|
||||||
display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast<unsigned>(data_rate / 1000000),
|
(madctl & MADCTL_BGR) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode,
|
||||||
bus_width);
|
static_cast<unsigned>(data_rate / 1000000), bus_width);
|
||||||
LOG_PIN(" CS Pin: ", cs);
|
LOG_PIN(" CS Pin: ", cs);
|
||||||
LOG_PIN(" Reset Pin: ", reset);
|
LOG_PIN(" Reset Pin: ", reset);
|
||||||
LOG_PIN(" DC Pin: ", dc);
|
LOG_PIN(" DC Pin: ", dc);
|
||||||
|
|||||||
@@ -34,13 +34,14 @@ static constexpr uint8_t SWIRE1 = 0x5A;
|
|||||||
static constexpr uint8_t SWIRE2 = 0x5B;
|
static constexpr uint8_t SWIRE2 = 0x5B;
|
||||||
static constexpr uint8_t PAGESEL = 0xFE;
|
static constexpr uint8_t PAGESEL = 0xFE;
|
||||||
|
|
||||||
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
||||||
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
|
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
|
||||||
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
|
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
|
||||||
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
|
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
|
||||||
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
|
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
|
||||||
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
||||||
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
||||||
|
static constexpr uint16_t MADCTL_FLIP_FLAG = 0x100; // controller uses axis flip bits
|
||||||
|
|
||||||
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||||
// store a 16 bit value in a buffer, big endian.
|
// store a 16 bit value in a buffer, big endian.
|
||||||
@@ -66,7 +67,8 @@ enum BusType {
|
|||||||
// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro
|
// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro
|
||||||
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
|
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
|
||||||
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
|
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
|
||||||
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width);
|
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width,
|
||||||
|
bool has_hardware_rotation);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for MIPI SPI displays.
|
* Base class for MIPI SPI displays.
|
||||||
@@ -83,7 +85,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
|
|||||||
* buffer
|
* buffer
|
||||||
*/
|
*/
|
||||||
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||||
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT>
|
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL, bool HAS_HARDWARE_ROTATION>
|
||||||
class MipiSpi : public display::Display,
|
class MipiSpi : public display::Display,
|
||||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||||
spi::DATA_RATE_1MHZ> {
|
spi::DATA_RATE_1MHZ> {
|
||||||
@@ -103,10 +105,39 @@ class MipiSpi : public display::Display,
|
|||||||
this->brightness_ = brightness;
|
this->brightness_ = brightness;
|
||||||
this->reset_params_();
|
this->reset_params_();
|
||||||
}
|
}
|
||||||
|
void set_rotation(display::DisplayRotation rotation) override {
|
||||||
|
this->rotation_ = rotation;
|
||||||
|
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||||
|
this->reset_params_();
|
||||||
|
}
|
||||||
|
}
|
||||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||||
|
|
||||||
int get_width_internal() override { return WIDTH; }
|
int get_width() override {
|
||||||
int get_height_internal() override { return HEIGHT; }
|
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||||
|
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||||
|
return HEIGHT;
|
||||||
|
return WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_height() override {
|
||||||
|
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||||
|
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||||
|
return WIDTH;
|
||||||
|
return HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If hardware rotation is in use, the actual display width/height changes with rotation
|
||||||
|
int get_width_internal() override {
|
||||||
|
if constexpr (HAS_HARDWARE_ROTATION)
|
||||||
|
return get_width();
|
||||||
|
return WIDTH;
|
||||||
|
}
|
||||||
|
int get_height_internal() override {
|
||||||
|
if constexpr (HAS_HARDWARE_ROTATION)
|
||||||
|
return get_height();
|
||||||
|
return HEIGHT;
|
||||||
|
}
|
||||||
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
||||||
|
|
||||||
// reset the display, and write the init sequence
|
// reset the display, and write the init sequence
|
||||||
@@ -166,9 +197,6 @@ class MipiSpi : public display::Display,
|
|||||||
case INVERT_ON:
|
case INVERT_ON:
|
||||||
this->invert_colors_ = true;
|
this->invert_colors_ = true;
|
||||||
break;
|
break;
|
||||||
case MADCTL_CMD:
|
|
||||||
this->madctl_ = arg_byte;
|
|
||||||
break;
|
|
||||||
case BRIGHTNESS:
|
case BRIGHTNESS:
|
||||||
this->brightness_ = arg_byte;
|
this->brightness_ = arg_byte;
|
||||||
break;
|
break;
|
||||||
@@ -177,13 +205,13 @@ class MipiSpi : public display::Display,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const auto *ptr = vec.data() + index;
|
const auto *ptr = vec.data() + index;
|
||||||
esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
|
|
||||||
this->write_command_(cmd, ptr, num_args);
|
this->write_command_(cmd, ptr, num_args);
|
||||||
index += num_args;
|
index += num_args;
|
||||||
if (cmd == SLEEP_OUT)
|
if (cmd == SLEEP_OUT)
|
||||||
delay(10);
|
delay(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this->reset_params_();
|
||||||
// init sequence no longer needed
|
// init sequence no longer needed
|
||||||
this->init_sequence_.clear();
|
this->init_sequence_.clear();
|
||||||
}
|
}
|
||||||
@@ -206,9 +234,10 @@ class MipiSpi : public display::Display,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dump_config() override {
|
void dump_config() override {
|
||||||
internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_,
|
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
|
||||||
DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_,
|
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
|
||||||
this->mode_, this->data_rate_, BUS_TYPE);
|
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||||
|
HAS_HARDWARE_ROTATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -219,10 +248,13 @@ class MipiSpi : public display::Display,
|
|||||||
|
|
||||||
// Writes a command to the display, with the given bytes.
|
// Writes a command to the display, with the given bytes.
|
||||||
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
|
||||||
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
|
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
|
||||||
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
// Don't spam the log after setup
|
||||||
#endif
|
if (this->init_sequence_.empty()) {
|
||||||
|
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||||
|
} else {
|
||||||
|
esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||||
|
}
|
||||||
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||||
this->enable();
|
this->enable();
|
||||||
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
||||||
@@ -271,16 +303,60 @@ class MipiSpi : public display::Display,
|
|||||||
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
||||||
if (this->brightness_.has_value())
|
if (this->brightness_.has_value())
|
||||||
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
||||||
|
|
||||||
|
// calculate new madctl value from base value adjusted for rotation
|
||||||
|
uint8_t madctl = MADCTL; // lower 8 bits only
|
||||||
|
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
|
||||||
|
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
|
||||||
|
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
|
||||||
|
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||||
|
switch (this->rotation_) {
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||||
|
madctl ^= x_mask; // flip X axis
|
||||||
|
madctl ^= MADCTL_MV; // swap X and Y axes
|
||||||
|
break;
|
||||||
|
case display::DISPLAY_ROTATION_180_DEGREES:
|
||||||
|
madctl ^= x_mask; // flip X axis
|
||||||
|
madctl ^= y_mask; // flip Y axis
|
||||||
|
break;
|
||||||
|
case display::DISPLAY_ROTATION_270_DEGREES:
|
||||||
|
madctl ^= y_mask; // flip Y axis
|
||||||
|
madctl ^= MADCTL_MV; // swap X and Y axes
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
esph_log_d(TAG, "Setting MADCTL for rotation %d, value %X", this->rotation_, madctl);
|
||||||
|
this->write_command_(MADCTL_CMD, madctl);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t get_offset_width_() {
|
||||||
|
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||||
|
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||||
|
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||||
|
return OFFSET_HEIGHT;
|
||||||
|
}
|
||||||
|
return OFFSET_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t get_offset_height_() {
|
||||||
|
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||||
|
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||||
|
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||||
|
return OFFSET_WIDTH;
|
||||||
|
}
|
||||||
|
return OFFSET_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the address window for the next data write
|
// set the address window for the next data write
|
||||||
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
||||||
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
|
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
|
||||||
uint8_t buf[4];
|
uint8_t buf[4];
|
||||||
x1 += OFFSET_WIDTH;
|
x1 += get_offset_width_();
|
||||||
x2 += OFFSET_WIDTH;
|
x2 += get_offset_width_();
|
||||||
y1 += OFFSET_HEIGHT;
|
y1 += get_offset_height_();
|
||||||
y2 += OFFSET_HEIGHT;
|
y2 += get_offset_height_();
|
||||||
put16_be(buf, y1);
|
put16_be(buf, y1);
|
||||||
put16_be(buf + 2, y2);
|
put16_be(buf + 2, y2);
|
||||||
this->write_command_(RASET, buf, sizeof buf);
|
this->write_command_(RASET, buf, sizeof buf);
|
||||||
@@ -408,7 +484,6 @@ class MipiSpi : public display::Display,
|
|||||||
optional<uint8_t> brightness_{};
|
optional<uint8_t> brightness_{};
|
||||||
const char *model_{"Unknown"};
|
const char *model_{"Unknown"};
|
||||||
std::vector<uint8_t> init_sequence_{};
|
std::vector<uint8_t> init_sequence_{};
|
||||||
uint8_t madctl_{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -427,22 +502,21 @@ class MipiSpi : public display::Display,
|
|||||||
* @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even)
|
* @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even)
|
||||||
*/
|
*/
|
||||||
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||||
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION,
|
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL,
|
||||||
int FRACTION, unsigned ROUNDING>
|
bool HAS_HARDWARE_ROTATION, int FRACTION, unsigned ROUNDING>
|
||||||
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
|
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
|
||||||
OFFSET_WIDTH, OFFSET_HEIGHT> {
|
OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION> {
|
||||||
public:
|
public:
|
||||||
// these values define the buffer size needed to write in accordance with the chip pixel alignment
|
// these values define the buffer size needed to write in accordance with the chip pixel alignment
|
||||||
// requirements. If the required rounding does not divide the width and height, we round up to the next multiple and
|
// requirements. If the required rounding does not divide the width and height, we round up to the next multiple and
|
||||||
// ignore the extra columns and rows when drawing, but use them to write to the display.
|
// ignore the extra columns and rows when drawing, but use them to write to the display.
|
||||||
static constexpr unsigned BUFFER_WIDTH = (WIDTH + ROUNDING - 1) / ROUNDING * ROUNDING;
|
static constexpr size_t round_buffer(size_t size) { return (size + ROUNDING - 1) / ROUNDING * ROUNDING; }
|
||||||
static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING;
|
|
||||||
|
|
||||||
MipiSpiBuffer() { this->rotation_ = ROTATION; }
|
MipiSpiBuffer() = default;
|
||||||
|
|
||||||
void dump_config() override {
|
void dump_config() override {
|
||||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||||
OFFSET_HEIGHT>::dump_config();
|
MADCTL, HAS_HARDWARE_ROTATION>::dump_config();
|
||||||
esph_log_config(TAG,
|
esph_log_config(TAG,
|
||||||
" Rotation: %d°\n"
|
" Rotation: %d°\n"
|
||||||
" Buffer pixels: %d bits\n"
|
" Buffer pixels: %d bits\n"
|
||||||
@@ -450,14 +524,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
" Buffer bytes: %zu\n"
|
" Buffer bytes: %zu\n"
|
||||||
" Draw rounding: %u",
|
" Draw rounding: %u",
|
||||||
this->rotation_, BUFFERPIXEL * 8, FRACTION,
|
this->rotation_, BUFFERPIXEL * 8, FRACTION,
|
||||||
sizeof(BUFFERTYPE) * BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION, ROUNDING);
|
sizeof(BUFFERTYPE) * round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION, ROUNDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup() override {
|
void setup() override {
|
||||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||||
OFFSET_HEIGHT>::setup();
|
MADCTL, HAS_HARDWARE_ROTATION>::setup();
|
||||||
RAMAllocator<BUFFERTYPE> allocator{};
|
RAMAllocator<BUFFERTYPE> allocator{};
|
||||||
this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION);
|
this->buffer_ = allocator.allocate(round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION);
|
||||||
if (this->buffer_ == nullptr) {
|
if (this->buffer_ == nullptr) {
|
||||||
this->mark_failed(LOG_STR("Buffer allocation failed"));
|
this->mark_failed(LOG_STR("Buffer allocation failed"));
|
||||||
}
|
}
|
||||||
@@ -472,11 +546,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
}
|
}
|
||||||
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
|
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
|
||||||
// the display height,
|
// the display height,
|
||||||
for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) {
|
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
|
||||||
|
this->start_line_ += this->get_height_internal() / FRACTION) {
|
||||||
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
auto lap = millis();
|
auto lap = millis();
|
||||||
#endif
|
#endif
|
||||||
this->end_line_ = this->start_line_ + HEIGHT / FRACTION;
|
this->end_line_ =
|
||||||
|
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
|
||||||
if (this->auto_clear_enabled_) {
|
if (this->auto_clear_enabled_) {
|
||||||
this->clear();
|
this->clear();
|
||||||
}
|
}
|
||||||
@@ -503,10 +579,10 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
int w = this->x_high_ - this->x_low_ + 1;
|
int w = this->x_high_ - this->x_low_ + 1;
|
||||||
int h = this->y_high_ - this->y_low_ + 1;
|
int h = this->y_high_ - this->y_low_ + 1;
|
||||||
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
||||||
this->y_low_ - this->start_line_, BUFFER_WIDTH - w);
|
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
|
||||||
// invalidate watermarks
|
// invalidate watermarks
|
||||||
this->x_low_ = WIDTH;
|
this->x_low_ = this->get_width_internal();
|
||||||
this->y_low_ = HEIGHT;
|
this->y_low_ = this->get_height_internal();
|
||||||
this->x_high_ = 0;
|
this->x_high_ = 0;
|
||||||
this->y_high_ = 0;
|
this->y_high_ = 0;
|
||||||
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
@@ -523,10 +599,23 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
void draw_pixel_at(int x, int y, Color color) override {
|
void draw_pixel_at(int x, int y, Color color) override {
|
||||||
if (!this->get_clipping().inside(x, y))
|
if (!this->get_clipping().inside(x, y))
|
||||||
return;
|
return;
|
||||||
rotate_coordinates(x, y);
|
if constexpr (not HAS_HARDWARE_ROTATION) {
|
||||||
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
|
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||||
|
x = WIDTH - x - 1;
|
||||||
|
y = HEIGHT - y - 1;
|
||||||
|
} else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) {
|
||||||
|
auto tmp = x;
|
||||||
|
x = WIDTH - y - 1;
|
||||||
|
y = tmp;
|
||||||
|
} else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) {
|
||||||
|
auto tmp = y;
|
||||||
|
y = HEIGHT - x - 1;
|
||||||
|
x = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x < 0 || x >= this->get_width_internal() || y < this->start_line_ || y >= this->end_line_)
|
||||||
return;
|
return;
|
||||||
this->buffer_[(y - this->start_line_) * BUFFER_WIDTH + x] = convert_color(color);
|
this->buffer_[(y - this->start_line_) * round_buffer(this->get_width_internal()) + x] = convert_color(color);
|
||||||
if (x < this->x_low_) {
|
if (x < this->x_low_) {
|
||||||
this->x_low_ = x;
|
this->x_low_ = x;
|
||||||
}
|
}
|
||||||
@@ -551,39 +640,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
|
|
||||||
this->x_low_ = 0;
|
this->x_low_ = 0;
|
||||||
this->y_low_ = this->start_line_;
|
this->y_low_ = this->start_line_;
|
||||||
this->x_high_ = WIDTH - 1;
|
this->x_high_ = this->get_width_internal() - 1;
|
||||||
this->y_high_ = this->end_line_ - 1;
|
this->y_high_ = this->end_line_ - 1;
|
||||||
std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color));
|
std::fill_n(this->buffer_, (this->end_line_ - this->start_line_) * round_buffer(this->get_width_internal()),
|
||||||
}
|
convert_color(color));
|
||||||
|
|
||||||
int get_width() override {
|
|
||||||
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
|
|
||||||
return HEIGHT;
|
|
||||||
return WIDTH;
|
|
||||||
}
|
|
||||||
|
|
||||||
int get_height() override {
|
|
||||||
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
|
|
||||||
return WIDTH;
|
|
||||||
return HEIGHT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Rotate the coordinates to match the display orientation.
|
// Rotate the coordinates to match the display orientation.
|
||||||
static void rotate_coordinates(int &x, int &y) {
|
|
||||||
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
|
|
||||||
x = WIDTH - x - 1;
|
|
||||||
y = HEIGHT - y - 1;
|
|
||||||
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) {
|
|
||||||
auto tmp = x;
|
|
||||||
x = WIDTH - y - 1;
|
|
||||||
y = tmp;
|
|
||||||
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) {
|
|
||||||
auto tmp = y;
|
|
||||||
y = HEIGHT - x - 1;
|
|
||||||
x = tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a color to the buffer pixel format.
|
// Convert a color to the buffer pixel format.
|
||||||
static BUFFERTYPE convert_color(const Color &color) {
|
static BUFFERTYPE convert_color(const Color &color) {
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ size_t ModbusController::create_register_ranges_() {
|
|||||||
while (ix != this->sensorset_.end()) {
|
while (ix != this->sensorset_.end()) {
|
||||||
SensorItem *curr = *ix;
|
SensorItem *curr = *ix;
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count,
|
ESP_LOGV(TAG, "Register: 0x%X %d %d %zu offset=%u skip=%u addr=%p", curr->start_address, curr->register_count,
|
||||||
curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr);
|
curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr);
|
||||||
|
|
||||||
if (r.register_count == 0) {
|
if (r.register_count == 0) {
|
||||||
@@ -484,18 +484,18 @@ void ModbusController::dump_config() {
|
|||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
ESP_LOGCONFIG(TAG, "sensormap");
|
ESP_LOGCONFIG(TAG, "sensormap");
|
||||||
for (auto &it : this->sensorset_) {
|
for (auto &it : this->sensorset_) {
|
||||||
ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d",
|
ESP_LOGCONFIG(TAG, " Sensor type=%u start=0x%X offset=0x%X count=%d size=%zu",
|
||||||
static_cast<uint8_t>(it->register_type), it->start_address, it->offset, it->register_count,
|
static_cast<uint8_t>(it->register_type), it->start_address, it->offset, it->register_count,
|
||||||
it->get_register_size());
|
it->get_register_size());
|
||||||
}
|
}
|
||||||
ESP_LOGCONFIG(TAG, "ranges");
|
ESP_LOGCONFIG(TAG, "ranges");
|
||||||
for (auto &it : this->register_ranges_) {
|
for (auto &it : this->register_ranges_) {
|
||||||
ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
|
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
|
||||||
it.start_address, it.register_count, it.skip_updates);
|
it.start_address, it.register_count, it.skip_updates);
|
||||||
}
|
}
|
||||||
ESP_LOGCONFIG(TAG, "server registers");
|
ESP_LOGCONFIG(TAG, "server registers");
|
||||||
for (auto &r : this->server_registers_) {
|
for (auto &r : this->server_registers_) {
|
||||||
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address,
|
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
|
||||||
static_cast<uint8_t>(r->value_type), r->register_count);
|
static_cast<uint8_t>(r->value_type), r->register_count);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -524,7 +524,7 @@ void ModbusController::on_write_register_response(ModbusRegisterType register_ty
|
|||||||
void ModbusController::dump_sensors_() {
|
void ModbusController::dump_sensors_() {
|
||||||
ESP_LOGV(TAG, "sensors");
|
ESP_LOGV(TAG, "sensors");
|
||||||
for (auto &it : this->sensorset_) {
|
for (auto &it : this->sensorset_) {
|
||||||
ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count,
|
ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%zu offset=%d", it->start_address, it->register_count,
|
||||||
it->get_register_size(), it->offset);
|
it->get_register_size(), it->offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
|
||||||
st7735_ns = cg.esphome_ns.namespace("st7735")
|
st7735_ns = cg.esphome_ns.namespace("st7735")
|
||||||
|
|
||||||
|
DEPRECATED_COMPONENT = """
|
||||||
|
The 'st7735' component is deprecated and no new models will be added to it.
|
||||||
|
New model PRs should target the newer and more performant 'mipi_spi' component.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
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
|
||||||
@@ -15,6 +17,7 @@ from esphome.const import (
|
|||||||
from . import st7735_ns
|
from . import st7735_ns
|
||||||
|
|
||||||
CODEOWNERS = ["@SenexCrenshaw"]
|
CODEOWNERS = ["@SenexCrenshaw"]
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ["spi"]
|
DEPENDENCIES = ["spi"]
|
||||||
|
|
||||||
@@ -87,6 +90,9 @@ async def setup_st7735(var, config):
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
|
LOGGER.warning(
|
||||||
|
"The 'st7735' component is deprecated, it is recommended to use 'mipi_spi' instead."
|
||||||
|
)
|
||||||
var = cg.new_Pvariable(
|
var = cg.new_Pvariable(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
config[CONF_MODEL],
|
config[CONF_MODEL],
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ bool CronTrigger::matches(const ESPTime &time) {
|
|||||||
return time.is_valid() && this->seconds_[time.second] && this->minutes_[time.minute] && this->hours_[time.hour] &&
|
return time.is_valid() && this->seconds_[time.second] && this->minutes_[time.minute] && this->hours_[time.hour] &&
|
||||||
this->days_of_month_[time.day_of_month] && this->months_[time.month] && this->days_of_week_[time.day_of_week];
|
this->days_of_month_[time.day_of_month] && this->months_[time.month] && this->days_of_week_[time.day_of_week];
|
||||||
}
|
}
|
||||||
void CronTrigger::loop() {
|
void CronTrigger::setup() {
|
||||||
|
// Cron resolution is 1 second — check once per second instead of every loop iteration
|
||||||
|
this->set_interval(1000, [this]() { this->check_time_(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void CronTrigger::check_time_() {
|
||||||
ESPTime time = this->rtc_->now();
|
ESPTime time = this->rtc_->now();
|
||||||
if (!time.is_valid())
|
if (!time.is_valid())
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ class CronTrigger : public Trigger<>, public Component {
|
|||||||
void add_day_of_week(uint8_t day_of_week);
|
void add_day_of_week(uint8_t day_of_week);
|
||||||
void add_days_of_week(const std::vector<uint8_t> &days_of_week);
|
void add_days_of_week(const std::vector<uint8_t> &days_of_week);
|
||||||
bool matches(const ESPTime &time);
|
bool matches(const ESPTime &time);
|
||||||
void loop() override;
|
void setup() override;
|
||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void check_time_();
|
||||||
std::bitset<61> seconds_;
|
std::bitset<61> seconds_;
|
||||||
std::bitset<60> minutes_;
|
std::bitset<60> minutes_;
|
||||||
std::bitset<24> hours_;
|
std::bitset<24> hours_;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ void UptimeTextSensor::update() {
|
|||||||
if (show_seconds)
|
if (show_seconds)
|
||||||
append_unit(buf, sizeof(buf), pos, this->separator_, seconds, this->seconds_text_);
|
append_unit(buf, sizeof(buf), pos, this->separator_, seconds, this->seconds_text_);
|
||||||
|
|
||||||
this->publish_state(buf);
|
this->publish_state(buf, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
|
float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||||
|
|||||||
@@ -286,10 +286,11 @@ void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char
|
|||||||
this->send(message, event, id, reconnect);
|
this->send(message, event, id, reconnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeferredUpdateEventSourceList::loop() {
|
bool DeferredUpdateEventSourceList::loop() {
|
||||||
for (DeferredUpdateEventSource *dues : *this) {
|
for (DeferredUpdateEventSource *dues : *this) {
|
||||||
dues->loop();
|
dues->loop();
|
||||||
}
|
}
|
||||||
|
return !this->empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type,
|
void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type,
|
||||||
@@ -318,6 +319,7 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer
|
|||||||
es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); });
|
es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); });
|
||||||
|
|
||||||
es->handleRequest(request);
|
es->handleRequest(request);
|
||||||
|
ws->enable_loop_soon_any_context();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) {
|
void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) {
|
||||||
@@ -413,13 +415,24 @@ void WebServer::setup() {
|
|||||||
// doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
|
// doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
|
||||||
// getting a lot of events
|
// getting a lot of events
|
||||||
this->set_interval(10000, [this]() {
|
this->set_interval(10000, [this]() {
|
||||||
|
if (this->events_.empty())
|
||||||
|
return;
|
||||||
char buf[32];
|
char buf[32];
|
||||||
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
|
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
|
||||||
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
|
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
|
||||||
this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
|
this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
void WebServer::loop() { this->events_.loop(); }
|
void WebServer::loop() {
|
||||||
|
// No SSE clients connected; stop looping until a new client connects via
|
||||||
|
// enable_loop_soon_any_context(). This is safe because:
|
||||||
|
// - set_interval/set_timeout/defer run via the Scheduler, independent of loop()
|
||||||
|
// - deferrable_send_state early-outs when no clients are connected
|
||||||
|
// - try_send_nodefer (log, ping) iterates sessions which are empty
|
||||||
|
// - REST API handlers use defer() which runs via the Scheduler
|
||||||
|
if (!this->events_.loop())
|
||||||
|
this->disable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef USE_LOGGER
|
#ifdef USE_LOGGER
|
||||||
void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||||
|
|||||||
@@ -169,7 +169,8 @@ class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEvent
|
|||||||
void on_client_disconnect_(DeferredUpdateEventSource *source);
|
void on_client_disconnect_(DeferredUpdateEventSource *source);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void loop();
|
/// Returns true if there are event sources remaining (including pending cleanup).
|
||||||
|
bool loop();
|
||||||
|
|
||||||
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
|
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
|
||||||
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
|
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
|
||||||
|
|||||||
@@ -484,9 +484,12 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
|
|||||||
this->on_connect_(rsp);
|
this->on_connect_(rsp);
|
||||||
}
|
}
|
||||||
this->sessions_.push_back(rsp);
|
this->sessions_.push_back(rsp);
|
||||||
|
// Wake up WebServer::loop() to drain deferred event queues for this client.
|
||||||
|
// Safe from httpd task context via the pending_enable_loop_ flag.
|
||||||
|
this->web_server_->enable_loop_soon_any_context();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AsyncEventSource::loop() {
|
bool AsyncEventSource::loop() {
|
||||||
// Clean up dead sessions safely
|
// Clean up dead sessions safely
|
||||||
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
|
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
|
||||||
// and the main loop handles the actual cleanup to avoid race conditions
|
// and the main loop handles the actual cleanup to avoid race conditions
|
||||||
@@ -504,6 +507,7 @@ void AsyncEventSource::loop() {
|
|||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return !this->sessions_.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
|
void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
|
||||||
|
|||||||
@@ -340,7 +340,8 @@ class AsyncEventSource : public AsyncWebHandler {
|
|||||||
|
|
||||||
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
|
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
|
||||||
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
|
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
|
||||||
void loop();
|
/// Returns true if there are sessions remaining (including pending cleanup).
|
||||||
|
bool loop();
|
||||||
bool empty() { return this->count() == 0; }
|
bool empty() { return this->count() == 0; }
|
||||||
|
|
||||||
size_t count() const { return this->sessions_.size(); }
|
size_t count() const { return this->sessions_.size(); }
|
||||||
|
|||||||
+2
-2
@@ -10,9 +10,9 @@ tzdata>=2021.1 # from time
|
|||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
platformio==6.1.19
|
platformio==6.1.19
|
||||||
esptool==5.2.0
|
esptool==5.2.0
|
||||||
click==8.3.1
|
click==8.3.2
|
||||||
esphome-dashboard==20260210.0
|
esphome-dashboard==20260210.0
|
||||||
aioesphomeapi==44.8.1
|
aioesphomeapi==44.9.0
|
||||||
zeroconf==0.148.0
|
zeroconf==0.148.0
|
||||||
puremagic==1.30
|
puremagic==1.30
|
||||||
ruamel.yaml==0.19.1 # dashboard_import
|
ruamel.yaml==0.19.1 # dashboard_import
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ def find_and_activate_virtualenv():
|
|||||||
def run_command():
|
def run_command():
|
||||||
# Execute the remaining arguments in the new environment
|
# Execute the remaining arguments in the new environment
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
subprocess.run(sys.argv[1:], check=False, close_fds=False)
|
result = subprocess.run(sys.argv[1:], check=False, close_fds=False)
|
||||||
|
sys.exit(result.returncode)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
"No command provided to run in the virtual environment.",
|
"No command provided to run in the virtual environment.",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from tests.testing_helpers import ComponentManifestOverride
|
||||||
|
|
||||||
|
|
||||||
|
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||||
|
manifest.enable_codegen()
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include "esphome/components/button/button.h"
|
||||||
|
|
||||||
|
namespace esphome::button::benchmarks {
|
||||||
|
|
||||||
|
static constexpr int kInnerIterations = 2000;
|
||||||
|
|
||||||
|
// Minimal Button for benchmarking — press_action() is a no-op.
|
||||||
|
class BenchButton : public Button {
|
||||||
|
public:
|
||||||
|
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void press_action() override {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Button::press() ---
|
||||||
|
// Measures: ESP_LOGD + press_action() + callback dispatch.
|
||||||
|
|
||||||
|
static void ButtonPress(benchmark::State &state) {
|
||||||
|
BenchButton button;
|
||||||
|
button.configure("test_button");
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
button.press();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(&button);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(ButtonPress);
|
||||||
|
|
||||||
|
// --- Button::press() with callback ---
|
||||||
|
// Measures callback dispatch overhead.
|
||||||
|
|
||||||
|
static void ButtonPress_WithCallback(benchmark::State &state) {
|
||||||
|
BenchButton button;
|
||||||
|
button.configure("test_button");
|
||||||
|
|
||||||
|
uint64_t callback_count = 0;
|
||||||
|
button.add_on_press_callback([&callback_count]() { callback_count++; });
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
button.press();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(callback_count);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(ButtonPress_WithCallback);
|
||||||
|
|
||||||
|
} // namespace esphome::button::benchmarks
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
button:
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from tests.testing_helpers import ComponentManifestOverride
|
||||||
|
|
||||||
|
|
||||||
|
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||||
|
manifest.enable_codegen()
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include "esphome/components/number/number.h"
|
||||||
|
|
||||||
|
namespace esphome::benchmarks {
|
||||||
|
|
||||||
|
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||||
|
static constexpr int kInnerIterations = 2000;
|
||||||
|
|
||||||
|
// Minimal Number for benchmarking — control() publishes the value back.
|
||||||
|
class BenchNumber : public number::Number {
|
||||||
|
public:
|
||||||
|
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void control(float value) override { this->publish_state(value); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to create a typical number entity for benchmarks.
|
||||||
|
static void setup_number(BenchNumber &number) {
|
||||||
|
number.configure("test_number");
|
||||||
|
number.traits.set_min_value(0.0f);
|
||||||
|
number.traits.set_max_value(100.0f);
|
||||||
|
number.traits.set_step(1.0f);
|
||||||
|
number.traits.set_mode(number::NUMBER_MODE_SLIDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Number::publish_state() ---
|
||||||
|
// Measures the publish path: set_has_state, store value, callback dispatch.
|
||||||
|
|
||||||
|
static void NumberPublish_State(benchmark::State &state) {
|
||||||
|
BenchNumber number;
|
||||||
|
setup_number(number);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
number.publish_state(static_cast<float>(i % 100));
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(number.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(NumberPublish_State);
|
||||||
|
|
||||||
|
// --- Number::publish_state() with callback ---
|
||||||
|
// Measures callback dispatch overhead.
|
||||||
|
|
||||||
|
static void NumberPublish_WithCallback(benchmark::State &state) {
|
||||||
|
BenchNumber number;
|
||||||
|
setup_number(number);
|
||||||
|
|
||||||
|
uint64_t callback_count = 0;
|
||||||
|
number.add_on_state_callback([&callback_count](float) { callback_count++; });
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
number.publish_state(static_cast<float>(i % 100));
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(callback_count);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(NumberPublish_WithCallback);
|
||||||
|
|
||||||
|
// --- NumberCall::perform() set value ---
|
||||||
|
// The most common number call — setting an absolute value.
|
||||||
|
// Exercises: validation against min/max, control() dispatch.
|
||||||
|
|
||||||
|
static void NumberCall_SetValue(benchmark::State &state) {
|
||||||
|
BenchNumber number;
|
||||||
|
setup_number(number);
|
||||||
|
number.publish_state(50.0f);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
float val = static_cast<float>(i % 100);
|
||||||
|
number.make_call().set_value(val).perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(number.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(NumberCall_SetValue);
|
||||||
|
|
||||||
|
// --- NumberCall::perform() increment ---
|
||||||
|
// Exercises: state read, step arithmetic, max clamping.
|
||||||
|
|
||||||
|
static void NumberCall_Increment(benchmark::State &state) {
|
||||||
|
BenchNumber number;
|
||||||
|
setup_number(number);
|
||||||
|
number.publish_state(0.0f);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
number.make_call().number_increment(true).perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(number.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(NumberCall_Increment);
|
||||||
|
|
||||||
|
// --- NumberCall::perform() decrement ---
|
||||||
|
// Exercises: state read, step arithmetic, min clamping.
|
||||||
|
|
||||||
|
static void NumberCall_Decrement(benchmark::State &state) {
|
||||||
|
BenchNumber number;
|
||||||
|
setup_number(number);
|
||||||
|
number.publish_state(100.0f);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
number.make_call().number_decrement(true).perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(number.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(NumberCall_Decrement);
|
||||||
|
|
||||||
|
} // namespace esphome::benchmarks
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
number:
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from tests.testing_helpers import ComponentManifestOverride
|
||||||
|
|
||||||
|
|
||||||
|
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||||
|
manifest.enable_codegen()
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include "esphome/components/select/select.h"
|
||||||
|
|
||||||
|
namespace esphome::benchmarks {
|
||||||
|
|
||||||
|
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||||
|
static constexpr int kInnerIterations = 2000;
|
||||||
|
|
||||||
|
// Minimal Select for benchmarking — control() publishes directly by index.
|
||||||
|
class BenchSelect : public select::Select {
|
||||||
|
public:
|
||||||
|
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void control(size_t index) override { this->publish_state(index); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to create a select with the given options.
|
||||||
|
static void setup_select(BenchSelect &select, const char *name, std::initializer_list<const char *> options) {
|
||||||
|
select.configure(name);
|
||||||
|
select.traits.set_options(options);
|
||||||
|
select.publish_state(size_t(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Select::publish_state(size_t) ---
|
||||||
|
// The fast path: publish by index, no string lookup.
|
||||||
|
|
||||||
|
static void SelectPublish_ByIndex(benchmark::State &state) {
|
||||||
|
BenchSelect select;
|
||||||
|
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
select.publish_state(static_cast<size_t>(i % 4));
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(select.active_index());
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SelectPublish_ByIndex);
|
||||||
|
|
||||||
|
// --- Select::publish_state(const char *) ---
|
||||||
|
// The string path: requires index_of() lookup via strncmp.
|
||||||
|
|
||||||
|
static void SelectPublish_ByString(benchmark::State &state) {
|
||||||
|
BenchSelect select;
|
||||||
|
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||||
|
|
||||||
|
const char *options[] = {"off", "still", "move", "still+move"};
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
select.publish_state(options[i % 4]);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(select.active_index());
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SelectPublish_ByString);
|
||||||
|
|
||||||
|
// --- Select::publish_state() with callback ---
|
||||||
|
// Measures callback dispatch overhead on the index path.
|
||||||
|
|
||||||
|
static void SelectPublish_WithCallback(benchmark::State &state) {
|
||||||
|
BenchSelect select;
|
||||||
|
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||||
|
|
||||||
|
uint64_t callback_count = 0;
|
||||||
|
select.add_on_state_callback([&callback_count](size_t) { callback_count++; });
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
select.publish_state(static_cast<size_t>(i % 4));
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(callback_count);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SelectPublish_WithCallback);
|
||||||
|
|
||||||
|
// --- SelectCall::perform() set by index ---
|
||||||
|
// The fast call path — no string matching needed.
|
||||||
|
|
||||||
|
static void SelectCall_SetByIndex(benchmark::State &state) {
|
||||||
|
BenchSelect select;
|
||||||
|
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
select.make_call().set_index(i % 4).perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(select.active_index());
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SelectCall_SetByIndex);
|
||||||
|
|
||||||
|
// --- SelectCall::perform() set by option string ---
|
||||||
|
// Exercises the string lookup path through index_of().
|
||||||
|
|
||||||
|
static void SelectCall_SetByOption(benchmark::State &state) {
|
||||||
|
BenchSelect select;
|
||||||
|
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||||
|
|
||||||
|
const char *options[] = {"off", "still", "move", "still+move"};
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
select.make_call().set_option(options[i % 4]).perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(select.active_index());
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SelectCall_SetByOption);
|
||||||
|
|
||||||
|
// --- SelectCall::perform() next with cycling ---
|
||||||
|
// Exercises the navigation path through active_index_.
|
||||||
|
|
||||||
|
static void SelectCall_NextCycle(benchmark::State &state) {
|
||||||
|
BenchSelect select;
|
||||||
|
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
select.make_call().select_next(true).perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(select.active_index());
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SelectCall_NextCycle);
|
||||||
|
|
||||||
|
// --- SelectCall with 10 options (string lookup) ---
|
||||||
|
// Worst-case string matching with more options.
|
||||||
|
|
||||||
|
static void SelectCall_SetByOption_10Options(benchmark::State &state) {
|
||||||
|
BenchSelect select;
|
||||||
|
setup_select(
|
||||||
|
select, "test_select",
|
||||||
|
{"off", "still", "move", "still+move", "custom1", "custom2", "custom3", "custom4", "custom5", "custom6"});
|
||||||
|
|
||||||
|
// Pick options spread across the list to exercise different search depths
|
||||||
|
const char *picks[] = {"off", "custom3", "custom6", "move"};
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
select.make_call().set_option(picks[i % 4]).perform();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(select.active_index());
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SelectCall_SetByOption_10Options);
|
||||||
|
|
||||||
|
} // namespace esphome::benchmarks
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
select:
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from tests.testing_helpers import ComponentManifestOverride
|
||||||
|
|
||||||
|
|
||||||
|
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||||
|
manifest.enable_codegen()
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include "esphome/components/switch/switch.h"
|
||||||
|
|
||||||
|
namespace esphome::benchmarks {
|
||||||
|
|
||||||
|
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||||
|
static constexpr int kInnerIterations = 2000;
|
||||||
|
|
||||||
|
// Minimal Switch for benchmarking — write_state() publishes directly.
|
||||||
|
class BenchSwitch : public switch_::Switch {
|
||||||
|
public:
|
||||||
|
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void write_state(bool state) override { this->publish_state(state); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Switch::publish_state() alternating ---
|
||||||
|
// Forces state change every call, exercising the full publish path.
|
||||||
|
|
||||||
|
static void SwitchPublish_Alternating(benchmark::State &state) {
|
||||||
|
BenchSwitch sw;
|
||||||
|
sw.configure("test_switch");
|
||||||
|
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||||
|
sw.publish_state(false);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sw.publish_state(i % 2 == 0);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(sw.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SwitchPublish_Alternating);
|
||||||
|
|
||||||
|
// --- Switch::publish_state() no change ---
|
||||||
|
// Tests the deduplication fast path in publish_dedup_.
|
||||||
|
|
||||||
|
static void SwitchPublish_NoChange(benchmark::State &state) {
|
||||||
|
BenchSwitch sw;
|
||||||
|
sw.configure("test_switch");
|
||||||
|
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||||
|
sw.publish_state(true);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sw.publish_state(true);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(sw.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SwitchPublish_NoChange);
|
||||||
|
|
||||||
|
// --- Switch::publish_state() with callback ---
|
||||||
|
// Measures callback dispatch overhead on state changes.
|
||||||
|
|
||||||
|
static void SwitchPublish_WithCallback(benchmark::State &state) {
|
||||||
|
BenchSwitch sw;
|
||||||
|
sw.configure("test_switch");
|
||||||
|
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||||
|
|
||||||
|
uint64_t callback_count = 0;
|
||||||
|
sw.add_on_state_callback([&callback_count](bool) { callback_count++; });
|
||||||
|
sw.publish_state(false);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sw.publish_state(i % 2 == 0);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(callback_count);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SwitchPublish_WithCallback);
|
||||||
|
|
||||||
|
// --- Switch::turn_on() / turn_off() ---
|
||||||
|
// The front-end call path: turn_on → write_state → publish_state.
|
||||||
|
|
||||||
|
static void SwitchTurnOn(benchmark::State &state) {
|
||||||
|
BenchSwitch sw;
|
||||||
|
sw.configure("test_switch");
|
||||||
|
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||||
|
sw.publish_state(false);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sw.turn_on();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(sw.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SwitchTurnOn);
|
||||||
|
|
||||||
|
// --- Switch::toggle() alternating ---
|
||||||
|
// Exercises the toggle path which reads current state to determine target.
|
||||||
|
|
||||||
|
static void SwitchToggle(benchmark::State &state) {
|
||||||
|
BenchSwitch sw;
|
||||||
|
sw.configure("test_switch");
|
||||||
|
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||||
|
sw.publish_state(false);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sw.toggle();
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(sw.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SwitchToggle);
|
||||||
|
|
||||||
|
// --- Switch::publish_state() inverted ---
|
||||||
|
// Verifies the inversion path doesn't add significant overhead.
|
||||||
|
|
||||||
|
static void SwitchPublish_Inverted(benchmark::State &state) {
|
||||||
|
BenchSwitch sw;
|
||||||
|
sw.configure("test_switch");
|
||||||
|
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||||
|
sw.set_inverted(true);
|
||||||
|
sw.publish_state(false);
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sw.publish_state(i % 2 == 0);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(sw.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(SwitchPublish_Inverted);
|
||||||
|
|
||||||
|
} // namespace esphome::benchmarks
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
switch:
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from tests.testing_helpers import ComponentManifestOverride
|
||||||
|
|
||||||
|
|
||||||
|
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||||
|
manifest.enable_codegen()
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include "esphome/components/text_sensor/text_sensor.h"
|
||||||
|
|
||||||
|
namespace esphome::text_sensor::benchmarks {
|
||||||
|
|
||||||
|
static constexpr int kInnerIterations = 2000;
|
||||||
|
|
||||||
|
// --- publish_state(const char *) with short string, value changes each time ---
|
||||||
|
// Exercises: memcmp check (mismatch), string assign, callback dispatch.
|
||||||
|
|
||||||
|
static void TextSensorPublish_Short_Changing(benchmark::State &state) {
|
||||||
|
TextSensor sensor;
|
||||||
|
|
||||||
|
// Pre-populate with different short strings
|
||||||
|
const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sensor.publish_state(values[i % 4]);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(sensor.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(TextSensorPublish_Short_Changing);
|
||||||
|
|
||||||
|
// --- publish_state(const char *) with short string, same value (dedup path) ---
|
||||||
|
// Exercises: memcmp check (match), skips string assign.
|
||||||
|
|
||||||
|
static void TextSensorPublish_Short_NoChange(benchmark::State &state) {
|
||||||
|
TextSensor sensor;
|
||||||
|
sensor.publish_state("192.168.1.100");
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sensor.publish_state("192.168.1.100");
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(sensor.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(TextSensorPublish_Short_NoChange);
|
||||||
|
|
||||||
|
// --- publish_state with longer string (firmware version, MAC address) ---
|
||||||
|
// Exercises: memcmp on longer strings, string assign with potential realloc.
|
||||||
|
|
||||||
|
static void TextSensorPublish_Long_Changing(benchmark::State &state) {
|
||||||
|
TextSensor sensor;
|
||||||
|
|
||||||
|
const char *values[] = {
|
||||||
|
"2025.12.0-dev (Jan 15 2025, 10:30:00)",
|
||||||
|
"2025.12.1-dev (Feb 20 2025, 14:45:00)",
|
||||||
|
"2025.12.2-dev (Mar 10 2025, 08:15:00)",
|
||||||
|
"2025.12.3-dev (Apr 5 2025, 16:00:00)",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sensor.publish_state(values[i % 4]);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(sensor.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(TextSensorPublish_Long_Changing);
|
||||||
|
|
||||||
|
// --- publish_state with callback ---
|
||||||
|
// Measures callback dispatch overhead for text sensors.
|
||||||
|
|
||||||
|
static void TextSensorPublish_WithCallback(benchmark::State &state) {
|
||||||
|
TextSensor sensor;
|
||||||
|
|
||||||
|
uint64_t callback_count = 0;
|
||||||
|
sensor.add_on_state_callback([&callback_count](const std::string &) { callback_count++; });
|
||||||
|
|
||||||
|
const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sensor.publish_state(values[i % 4]);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(callback_count);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(TextSensorPublish_WithCallback);
|
||||||
|
|
||||||
|
// --- publish_state(const char *, size_t) direct ---
|
||||||
|
// The lowest-level overload, avoids strlen.
|
||||||
|
|
||||||
|
static void TextSensorPublish_WithLen(benchmark::State &state) {
|
||||||
|
TextSensor sensor;
|
||||||
|
|
||||||
|
static constexpr const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
|
||||||
|
static constexpr size_t lens[] = {11, 11, 11, 11};
|
||||||
|
|
||||||
|
for (auto _ : state) {
|
||||||
|
for (int i = 0; i < kInnerIterations; i++) {
|
||||||
|
sensor.publish_state(values[i % 4], lens[i % 4]);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(sensor.state);
|
||||||
|
}
|
||||||
|
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||||
|
}
|
||||||
|
BENCHMARK(TextSensorPublish_WithLen);
|
||||||
|
|
||||||
|
} // namespace esphome::text_sensor::benchmarks
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
text_sensor:
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user