mirror of
https://github.com/esphome/esphome.git
synced 2026-05-29 23:07:16 +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
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
// Get base branch ref to check if deprecation already exists for the component
|
||||
// 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
|
||||
for (const component of components) {
|
||||
const initFile = `esphome/components/${component}/__init__.py`;
|
||||
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({
|
||||
owner,
|
||||
repo,
|
||||
path: initFile,
|
||||
ref: `refs/pull/${prNumber}/head`
|
||||
ref: baseRef
|
||||
});
|
||||
|
||||
// Decode base64 content
|
||||
|
||||
@@ -723,7 +723,7 @@ jobs:
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
||||
env:
|
||||
SKIP: pylint,clang-tidy-hash
|
||||
SKIP: pylint,clang-tidy-hash,ci-custom
|
||||
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
||||
if: always()
|
||||
|
||||
|
||||
@@ -65,3 +65,7 @@ repos:
|
||||
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
|
||||
pass_filenames: false
|
||||
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_POWER = "mdi:solar-power"
|
||||
|
||||
KEY_METADATA = "metadata"
|
||||
|
||||
UNIT_AMPERE_HOUR = "Ah"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from esphome import automation, core
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import KEY_METADATA
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
@@ -16,7 +19,9 @@ from esphome.const import (
|
||||
SCHEDULER_DONT_RUN,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
DOMAIN = "display"
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
display_ns = cg.esphome_ns.namespace("display")
|
||||
@@ -146,6 +151,39 @@ async def setup_display_core_(var, config):
|
||||
cg.add(var.show_test_card())
|
||||
|
||||
|
||||
# Storage of display metadata in a central location, accessible via the id
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DisplayMetaData:
|
||||
width: int = 0
|
||||
height: int = 0
|
||||
has_writer: bool = False
|
||||
has_hardware_rotation: bool = False
|
||||
|
||||
|
||||
def get_all_display_metadata() -> dict[str, DisplayMetaData]:
|
||||
"""Get all display metadata."""
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
|
||||
|
||||
|
||||
def get_display_metadata(display_id: str) -> DisplayMetaData | None:
|
||||
"""Get display metadata by ID for use by other components."""
|
||||
return get_all_display_metadata().get(display_id, DisplayMetaData())
|
||||
|
||||
|
||||
def add_metadata(
|
||||
id: str | MockObj,
|
||||
width: int,
|
||||
height: int,
|
||||
has_writer: bool,
|
||||
has_hardware_rotation: bool = False,
|
||||
):
|
||||
get_all_display_metadata()[str(id)] = DisplayMetaData(
|
||||
width, height, has_writer, has_hardware_rotation
|
||||
)
|
||||
|
||||
|
||||
async def register_display(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await setup_display_core_(var, config)
|
||||
|
||||
@@ -704,7 +704,7 @@ class Display : public PollingComponent {
|
||||
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
|
||||
|
||||
/// Internal method to set the display rotation with.
|
||||
void set_rotation(DisplayRotation rotation);
|
||||
virtual void set_rotation(DisplayRotation rotation);
|
||||
|
||||
// Internal method to set display auto clearing.
|
||||
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
|
||||
|
||||
@@ -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])
|
||||
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_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -5,11 +5,17 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
|
||||
#include <csignal>
|
||||
#include <sched.h>
|
||||
#include <time.h>
|
||||
#include <cmath>
|
||||
#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 {
|
||||
|
||||
void HOT yield() { ::sched_yield(); }
|
||||
@@ -72,11 +78,17 @@ uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
|
||||
void setup();
|
||||
void loop();
|
||||
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();
|
||||
setup();
|
||||
while (true) {
|
||||
while (s_signal_received == 0) {
|
||||
loop();
|
||||
}
|
||||
esphome::App.run_safe_shutdown_hooks();
|
||||
return 0;
|
||||
}
|
||||
|
||||
#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")
|
||||
|
||||
return spi.final_validate_device_schema(
|
||||
"ili9xxx", require_miso=False, require_mosi=True
|
||||
spi.final_validate_device_schema("ili9xxx", require_miso=False, require_mosi=True)(
|
||||
config
|
||||
)
|
||||
|
||||
|
||||
@@ -219,6 +219,9 @@ FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
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()
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from esphome import core, external_files
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_BYTE_ORDER
|
||||
from esphome.components.const import CONF_BYTE_ORDER, KEY_METADATA
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEFAULTS,
|
||||
@@ -53,7 +53,6 @@ CONF_CHROMA_KEY = "chroma_key"
|
||||
CONF_ALPHA_CHANNEL = "alpha_channel"
|
||||
CONF_INVERT_ALPHA = "invert_alpha"
|
||||
CONF_IMAGES = "images"
|
||||
KEY_METADATA = "metadata"
|
||||
|
||||
TRANSPARENCY_TYPES = (
|
||||
CONF_OPAQUE,
|
||||
|
||||
@@ -23,6 +23,7 @@ from esphome.core.config import StartupTrigger
|
||||
|
||||
from . import defines as df, lv_validation as lvalid
|
||||
from .defines import (
|
||||
CONF_EXT_CLICK_AREA,
|
||||
CONF_SCROLL_DIR,
|
||||
CONF_SCROLL_SNAP_X,
|
||||
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(
|
||||
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
|
||||
).one_of,
|
||||
cv.Optional(CONF_EXT_CLICK_AREA): lvalid.pixels,
|
||||
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_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 = {
|
||||
CONF_EXT_CLICK_AREA,
|
||||
CONF_SCROLL_SNAP_X,
|
||||
CONF_SCROLL_SNAP_Y,
|
||||
CONF_SCROLL_DIR,
|
||||
@@ -433,7 +436,6 @@ def obj_schema(widget_type: WidgetType):
|
||||
return (
|
||||
part_schema(widget_type.parts)
|
||||
.extend(ALIGN_TO_SCHEMA)
|
||||
.extend({cv.Optional(df.CONF_EXT_CLICK_AREA): lvalid.pixels})
|
||||
.extend(automation_schema(widget_type.w_type))
|
||||
.extend(
|
||||
{
|
||||
|
||||
@@ -15,7 +15,6 @@ from .defines import (
|
||||
CONF_ALIGN,
|
||||
CONF_ALIGN_TO,
|
||||
CONF_ALIGN_TO_LAMBDA_ID,
|
||||
CONF_EXT_CLICK_AREA,
|
||||
DIRECTIONS,
|
||||
LV_EVENT_MAP,
|
||||
LV_EVENT_TRIGGERS,
|
||||
@@ -114,8 +113,6 @@ async def generate_align_tos(config: dict):
|
||||
x = align_to[CONF_X]
|
||||
y = align_to[CONF_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]
|
||||
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_YFLIP = 0x01 # Mirror the display vertically
|
||||
|
||||
MADCTL_FLIP_FLAG = 0x100 # meta-flag to indicate use of axis flips
|
||||
|
||||
# Special constant for delays in command sequences
|
||||
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
|
||||
|
||||
@@ -329,7 +331,13 @@ class DriverChip:
|
||||
return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms
|
||||
return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms
|
||||
|
||||
def get_dimensions(self, config) -> tuple[int, int, int, int]:
|
||||
def get_dimensions(self, config, swap: bool = True) -> tuple[int, int, int, int]:
|
||||
"""
|
||||
Return the dimensions of the current model.
|
||||
:param config: The current configuration
|
||||
:param swap: If width/height should be swapped when axes are swapped.
|
||||
:return:
|
||||
"""
|
||||
if CONF_DIMENSIONS in config:
|
||||
# Explicit dimensions, just use as is
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
@@ -361,13 +369,12 @@ class DriverChip:
|
||||
)
|
||||
offset_height = native_height - height - offset_height
|
||||
# Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer
|
||||
if transform.get(CONF_SWAP_XY) is True:
|
||||
if swap and transform.get(CONF_SWAP_XY) is True:
|
||||
width, height = height, width
|
||||
offset_height, offset_width = offset_width, offset_height
|
||||
return width, height, offset_width, offset_height
|
||||
|
||||
def get_transform(self, config) -> dict[str, bool]:
|
||||
can_transform = self.rotation_as_transform(config)
|
||||
def get_base_transform(self, config):
|
||||
transform = config.get(
|
||||
CONF_TRANSFORM,
|
||||
{
|
||||
@@ -376,14 +383,20 @@ class DriverChip:
|
||||
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
|
||||
},
|
||||
)
|
||||
if not isinstance(transform, dict):
|
||||
# Presumably disabled
|
||||
return {
|
||||
CONF_MIRROR_X: False,
|
||||
CONF_MIRROR_Y: False,
|
||||
CONF_SWAP_XY: False,
|
||||
CONF_TRANSFORM: False,
|
||||
}
|
||||
if isinstance(transform, dict):
|
||||
return transform
|
||||
|
||||
# Transform is disabled
|
||||
return {
|
||||
CONF_MIRROR_X: False,
|
||||
CONF_MIRROR_Y: False,
|
||||
CONF_SWAP_XY: False,
|
||||
CONF_TRANSFORM: False,
|
||||
}
|
||||
|
||||
def get_transform(self, config) -> dict[str, bool]:
|
||||
transform = self.get_base_transform(config)
|
||||
can_transform = self.rotation_as_transform(config)
|
||||
# Can we use the MADCTL register to set the rotation?
|
||||
if can_transform and CONF_TRANSFORM not in config:
|
||||
rotation = config[CONF_ROTATION]
|
||||
@@ -411,11 +424,15 @@ class DriverChip:
|
||||
return {cv.Required(CONF_SWAP_XY): cv.boolean}
|
||||
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
|
||||
|
||||
def add_madctl(self, sequence: list, config: dict):
|
||||
# Add the MADCTL command to the sequence based on the configuration.
|
||||
use_flip = config.get(CONF_USE_AXIS_FLIPS)
|
||||
madctl = 0
|
||||
transform = self.get_transform(config)
|
||||
def get_madctl(self, transform: dict, config: dict) -> int:
|
||||
"""
|
||||
Convert a transform to MADCTL bits
|
||||
:param transform: The transform dict
|
||||
:param use_flip: Whether to use axis flips
|
||||
:return: MADCTL value
|
||||
"""
|
||||
use_flip = config.get(CONF_USE_AXIS_FLIPS, False)
|
||||
madctl = MADCTL_FLIP_FLAG if use_flip else 0
|
||||
if transform[CONF_MIRROR_X]:
|
||||
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
|
||||
if transform[CONF_MIRROR_Y]:
|
||||
@@ -424,22 +441,28 @@ class DriverChip:
|
||||
madctl |= MADCTL_MV
|
||||
if config[CONF_COLOR_ORDER] == MODE_BGR:
|
||||
madctl |= MADCTL_BGR
|
||||
sequence.append((MADCTL, madctl))
|
||||
return madctl
|
||||
|
||||
def add_madctl(self, sequence: list, config: dict):
|
||||
# Add the MADCTL command to the sequence based on the configuration.
|
||||
# This takes into account rotation if it can be implemented in the transform
|
||||
transform = self.get_transform(config)
|
||||
madctl = self.get_madctl(transform, config)
|
||||
sequence.append((MADCTL, madctl & 0xFF))
|
||||
|
||||
def skip_command(self, command: str):
|
||||
"""
|
||||
Allow suppressing a standard command in the init sequence.
|
||||
"""
|
||||
return self.get_default(f"no_{command.lower()}", False)
|
||||
|
||||
def get_sequence(self, config) -> tuple[tuple[int, ...], int]:
|
||||
def get_sequence(self, config, add_madctl=True) -> tuple[int, ...]:
|
||||
"""
|
||||
Create the init sequence for the display.
|
||||
Use the default sequence from the model, if any, and append any custom sequence provided in the config.
|
||||
Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence
|
||||
Pixel format, color order, and orientation will be set.
|
||||
Returns a tuple of the init sequence and the computed MADCTL value.
|
||||
MADCTL will be set if add_madctl is True
|
||||
Returns the init sequence
|
||||
"""
|
||||
sequence = list(self.initsequence or ())
|
||||
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
|
||||
@@ -457,7 +480,8 @@ class DriverChip:
|
||||
|
||||
if self.rotation_as_transform(config):
|
||||
LOGGER.info("Using hardware transform to implement rotation")
|
||||
madctl = self.add_madctl(sequence, config)
|
||||
if add_madctl:
|
||||
self.add_madctl(sequence, config)
|
||||
if config[CONF_INVERT_COLORS]:
|
||||
sequence.append((INVON,))
|
||||
else:
|
||||
@@ -471,7 +495,7 @@ class DriverChip:
|
||||
|
||||
# Flatten the sequence into a list of bytes, with the length of each command
|
||||
# or the delay flag inserted where needed
|
||||
return flatten_sequence(sequence), madctl
|
||||
return flatten_sequence(sequence)
|
||||
|
||||
|
||||
def requires_buffer(config) -> bool:
|
||||
|
||||
@@ -192,10 +192,9 @@ async def to_code(config):
|
||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode)
|
||||
|
||||
sequence, madctl = model.get_sequence(config)
|
||||
sequence = model.get_sequence(config)
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
cg.add(var.set_init_sequence(sequence))
|
||||
cg.add(var.set_madctl(madctl))
|
||||
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
|
||||
cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH]))
|
||||
cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH]))
|
||||
|
||||
@@ -392,9 +392,6 @@ void MIPI_DSI::dump_config() {
|
||||
"\n Model: %s"
|
||||
"\n Width: %u"
|
||||
"\n Height: %u"
|
||||
"\n Mirror X: %s"
|
||||
"\n Mirror Y: %s"
|
||||
"\n Swap X/Y: %s"
|
||||
"\n Rotation: %d degrees"
|
||||
"\n DSI Lanes: %u"
|
||||
"\n Lane Bit Rate: %.0fMbps"
|
||||
@@ -406,14 +403,11 @@ void MIPI_DSI::dump_config() {
|
||||
"\n VSync Front Porch: %u"
|
||||
"\n Buffer Color Depth: %d bit"
|
||||
"\n Display Pixel Mode: %d bit"
|
||||
"\n Color Order: %s"
|
||||
"\n Invert Colors: %s"
|
||||
"\n Pixel Clock: %.1fMHz",
|
||||
this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)),
|
||||
YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_,
|
||||
this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_,
|
||||
this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, this->vsync_front_porch_,
|
||||
(3 - this->color_depth_) * 8, this->pixel_mode_, this->madctl_ & MADCTL_BGR ? "BGR" : "RGB",
|
||||
this->model_, this->width_, this->height_, this->rotation_, this->lanes_, this->lane_bit_rate_,
|
||||
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
|
||||
this->vsync_back_porch_, this->vsync_front_porch_, (3 - this->color_depth_) * 8, this->pixel_mode_,
|
||||
YESNO(this->invert_colors_), this->pclk_frequency_);
|
||||
LOG_PIN(" Reset Pin ", this->reset_pin_);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ class MIPI_DSI : public display::Display {
|
||||
void set_model(const char *model) { this->model_ = model; }
|
||||
void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; }
|
||||
void set_lanes(uint8_t lanes) { this->lanes_ = lanes; }
|
||||
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
|
||||
|
||||
void smark_failed(const LogString *message, esp_err_t err);
|
||||
|
||||
@@ -86,7 +85,6 @@ class MIPI_DSI : public display::Display {
|
||||
std::vector<GPIOPin *> enable_pins_{};
|
||||
size_t width_{};
|
||||
size_t height_{};
|
||||
uint8_t madctl_{};
|
||||
uint16_t hsync_pulse_width_ = 10;
|
||||
uint16_t hsync_back_porch_ = 10;
|
||||
uint16_t hsync_front_porch_ = 20;
|
||||
|
||||
@@ -265,9 +265,8 @@ async def to_code(config):
|
||||
|
||||
if CONF_SPI_ID in config:
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
sequence, madctl = model.get_sequence(config)
|
||||
sequence = model.get_sequence(config)
|
||||
cg.add(var.set_init_sequence(sequence))
|
||||
cg.add(var.set_madctl(madctl))
|
||||
|
||||
cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
|
||||
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
|
||||
|
||||
@@ -118,15 +118,7 @@ void MipiRgbSpi::dump_config() {
|
||||
MipiRgb::dump_config();
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" SPI Data rate: %uMHz"
|
||||
"\n Mirror X: %s"
|
||||
"\n Mirror Y: %s"
|
||||
"\n Swap X/Y: %s"
|
||||
"\n Color Order: %s",
|
||||
(unsigned) (this->data_rate_ / 1000000), YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)),
|
||||
YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY | MADCTL_ML)), YESNO(this->madctl_ & MADCTL_MV),
|
||||
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB");
|
||||
ESP_LOGCONFIG(TAG, " SPI Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000));
|
||||
}
|
||||
|
||||
#endif // USE_SPI
|
||||
|
||||
@@ -38,7 +38,6 @@ class MipiRgb : public display::Display {
|
||||
display::ColorOrder get_color_mode() { return this->color_mode_; }
|
||||
void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; }
|
||||
void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
|
||||
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
|
||||
|
||||
void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; };
|
||||
void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; }
|
||||
@@ -84,7 +83,6 @@ class MipiRgb : public display::Display {
|
||||
uint16_t vsync_front_porch_ = 10;
|
||||
uint32_t pclk_frequency_ = 16 * 1000 * 1000;
|
||||
bool pclk_inverted_{true};
|
||||
uint8_t madctl_{};
|
||||
const char *model_{"Unknown"};
|
||||
bool invert_colors_{};
|
||||
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
|
||||
|
||||
@@ -10,7 +10,7 @@ from esphome.components.const import (
|
||||
CONF_COLOR_DEPTH,
|
||||
CONF_DRAW_ROUNDING,
|
||||
)
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD
|
||||
from esphome.components.mipi import (
|
||||
CONF_PIXEL_MODE,
|
||||
CONF_USE_AXIS_FLIPS,
|
||||
@@ -47,12 +47,10 @@ from esphome.const import (
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_RESET_PIN,
|
||||
CONF_ROTATION,
|
||||
CONF_SWAP_XY,
|
||||
CONF_TRANSFORM,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import TemplateArguments
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
@@ -113,22 +111,21 @@ DISPLAY_PIXEL_MODES = {
|
||||
def denominator(config):
|
||||
"""
|
||||
Calculate the best denominator for a buffer size fraction.
|
||||
The denominator must be a number between 2 and 16 that divides the display height evenly,
|
||||
The denominator should be a number between 2 and 16 that divides the display height evenly,
|
||||
and the fraction represented by the denominator must be less than or equal to the given fraction.
|
||||
:config: The configuration dictionary containing the buffer size fraction and display dimensions
|
||||
:return: The denominator to use for the buffer size fraction
|
||||
"""
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
frac = config.get(CONF_BUFFER_SIZE)
|
||||
if frac is None or frac > 0.75:
|
||||
_width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
if frac is None or frac > 0.75 or height < 32:
|
||||
return 1
|
||||
height, _width, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
try:
|
||||
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
|
||||
except StopIteration:
|
||||
raise cv.Invalid(
|
||||
f"Buffer size fraction {frac} is not compatible with display height {height}"
|
||||
) from StopIteration
|
||||
# No exact divisor, just use the closest.
|
||||
return next(x for x in range(2, 17) if frac >= 1 / x)
|
||||
|
||||
|
||||
def model_schema(config):
|
||||
@@ -287,30 +284,19 @@ def _final_validate(config):
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
|
||||
if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
|
||||
if not requires_buffer(config):
|
||||
return config # No buffer needed, so no need to set a buffer size
|
||||
# If PSRAM is not enabled, choose a small buffer size by default
|
||||
if not requires_buffer(config):
|
||||
# not our problem.
|
||||
return config
|
||||
return config # No buffer needed, so no need to set a buffer size
|
||||
color_depth = get_color_depth(config)
|
||||
frac = denominator(config)
|
||||
height, width, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
|
||||
buffer_size = color_depth // 8 * width * height // frac
|
||||
# Target a buffer size of 20kB
|
||||
fraction = 20000.0 / buffer_size
|
||||
try:
|
||||
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
||||
x for x in range(2, 17) if fraction >= 1 / x and height % x == 0
|
||||
)
|
||||
except StopIteration:
|
||||
# Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0
|
||||
# PSRAM will be needed.
|
||||
if CORE.is_esp32:
|
||||
raise cv.Invalid(
|
||||
"PSRAM is required for this display"
|
||||
) from StopIteration
|
||||
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
|
||||
fraction = min(20000.0, buffer_size // 16) / buffer_size
|
||||
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
||||
x for x in range(2, 17) if fraction >= 1 / x
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
@@ -318,39 +304,6 @@ def _final_validate(config):
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def get_transform(config):
|
||||
"""
|
||||
Get the transformation configuration for the display.
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
can_transform = model.rotation_as_transform(config)
|
||||
transform = config.get(
|
||||
CONF_TRANSFORM,
|
||||
{
|
||||
CONF_MIRROR_X: model.get_default(CONF_MIRROR_X, False),
|
||||
CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y, False),
|
||||
CONF_SWAP_XY: model.get_default(CONF_SWAP_XY, False),
|
||||
},
|
||||
)
|
||||
|
||||
# Can we use the MADCTL register to set the rotation?
|
||||
if can_transform and CONF_TRANSFORM not in config:
|
||||
rotation = config[CONF_ROTATION]
|
||||
if rotation == 180:
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
elif rotation == 90:
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
else:
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
transform[CONF_TRANSFORM] = True
|
||||
return transform
|
||||
|
||||
|
||||
def get_instance(config):
|
||||
"""
|
||||
Get the type of MipiSpi instance to create based on the configuration,
|
||||
@@ -359,7 +312,16 @@ def get_instance(config):
|
||||
:return: type, template arguments
|
||||
"""
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
width, height, offset_width, offset_height = model.get_dimensions(config)
|
||||
has_hardware_transform = config.get(
|
||||
CONF_TRANSFORM
|
||||
) != CONF_DISABLED and model.transforms == {
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_SWAP_XY,
|
||||
}
|
||||
width, height, offset_width, offset_height = model.get_dimensions(
|
||||
config, not has_hardware_transform
|
||||
)
|
||||
|
||||
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
|
||||
bufferpixels = COLOR_DEPTHS[color_depth]
|
||||
@@ -373,57 +335,43 @@ def get_instance(config):
|
||||
bus_type = BusTypes[bus_type]
|
||||
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
|
||||
frac = denominator(config)
|
||||
rotation = (
|
||||
0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0)
|
||||
)
|
||||
madctl = model.get_madctl(model.get_base_transform(config), config)
|
||||
has_writer = requires_buffer(config)
|
||||
templateargs = [
|
||||
buffer_type,
|
||||
bufferpixels,
|
||||
config[CONF_BYTE_ORDER] == "big_endian",
|
||||
display_pixel_mode,
|
||||
bus_type,
|
||||
width,
|
||||
height,
|
||||
offset_width,
|
||||
offset_height,
|
||||
madctl,
|
||||
has_hardware_transform,
|
||||
]
|
||||
display.add_metadata(
|
||||
config[CONF_ID], width, height, has_writer, has_hardware_transform
|
||||
)
|
||||
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
|
||||
if requires_buffer(config):
|
||||
templateargs.extend(
|
||||
[
|
||||
width,
|
||||
height,
|
||||
offset_width,
|
||||
offset_height,
|
||||
DISPLAY_ROTATIONS[rotation],
|
||||
frac,
|
||||
config[CONF_DRAW_ROUNDING],
|
||||
]
|
||||
)
|
||||
return MipiSpiBuffer, templateargs
|
||||
# Swap height and width if the display is rotated 90 or 270 degrees in software
|
||||
if rotation in (90, 270):
|
||||
width, height = height, width
|
||||
offset_width, offset_height = offset_height, offset_width
|
||||
templateargs.extend(
|
||||
[
|
||||
width,
|
||||
height,
|
||||
offset_width,
|
||||
offset_height,
|
||||
]
|
||||
)
|
||||
return MipiSpi, templateargs
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
var_id = config[CONF_ID]
|
||||
init_sequence = model.get_sequence(config, False)
|
||||
var_id.type, templateargs = get_instance(config)
|
||||
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
|
||||
init_sequence, _madctl = model.get_sequence(config)
|
||||
cg.add(var.set_init_sequence(init_sequence))
|
||||
if model.rotation_as_transform(config):
|
||||
if CONF_TRANSFORM in config:
|
||||
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
|
||||
else:
|
||||
config[CONF_ROTATION] = 0
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
|
||||
|
||||
@@ -5,7 +5,8 @@ namespace esphome::mipi_spi {
|
||||
|
||||
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
|
||||
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
|
||||
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) {
|
||||
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width,
|
||||
bool has_hardware_rotation) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"MIPI_SPI Display\n"
|
||||
" Model: %s\n"
|
||||
@@ -14,6 +15,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
|
||||
" Swap X/Y: %s\n"
|
||||
" Mirror X: %s\n"
|
||||
" Mirror Y: %s\n"
|
||||
" Hardware rotation: %s\n"
|
||||
" Invert colors: %s\n"
|
||||
" Color order: %s\n"
|
||||
" Display pixels: %d bits\n"
|
||||
@@ -22,9 +24,9 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
|
||||
" SPI Data rate: %uMHz\n"
|
||||
" SPI Bus width: %d",
|
||||
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
|
||||
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB",
|
||||
display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast<unsigned>(data_rate / 1000000),
|
||||
bus_width);
|
||||
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(has_hardware_rotation), YESNO(invert_colors),
|
||||
(madctl & MADCTL_BGR) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode,
|
||||
static_cast<unsigned>(data_rate / 1000000), bus_width);
|
||||
LOG_PIN(" CS Pin: ", cs);
|
||||
LOG_PIN(" Reset Pin: ", reset);
|
||||
LOG_PIN(" DC Pin: ", dc);
|
||||
|
||||
@@ -34,13 +34,14 @@ static constexpr uint8_t SWIRE1 = 0x5A;
|
||||
static constexpr uint8_t SWIRE2 = 0x5B;
|
||||
static constexpr uint8_t PAGESEL = 0xFE;
|
||||
|
||||
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
||||
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
|
||||
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
|
||||
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
|
||||
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
|
||||
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
||||
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
||||
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
||||
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
|
||||
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
|
||||
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
|
||||
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
|
||||
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
||||
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
||||
static constexpr uint16_t MADCTL_FLIP_FLAG = 0x100; // controller uses axis flip bits
|
||||
|
||||
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||
// store a 16 bit value in a buffer, big endian.
|
||||
@@ -66,7 +67,8 @@ enum BusType {
|
||||
// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro
|
||||
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
|
||||
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
|
||||
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width);
|
||||
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width,
|
||||
bool has_hardware_rotation);
|
||||
|
||||
/**
|
||||
* Base class for MIPI SPI displays.
|
||||
@@ -83,7 +85,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
|
||||
* buffer
|
||||
*/
|
||||
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT>
|
||||
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL, bool HAS_HARDWARE_ROTATION>
|
||||
class MipiSpi : public display::Display,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
@@ -103,10 +105,39 @@ class MipiSpi : public display::Display,
|
||||
this->brightness_ = brightness;
|
||||
this->reset_params_();
|
||||
}
|
||||
void set_rotation(display::DisplayRotation rotation) override {
|
||||
this->rotation_ = rotation;
|
||||
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||
this->reset_params_();
|
||||
}
|
||||
}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
int get_width_internal() override { return WIDTH; }
|
||||
int get_height_internal() override { return HEIGHT; }
|
||||
int get_width() override {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return HEIGHT;
|
||||
return WIDTH;
|
||||
}
|
||||
|
||||
int get_height() override {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return WIDTH;
|
||||
return HEIGHT;
|
||||
}
|
||||
|
||||
// If hardware rotation is in use, the actual display width/height changes with rotation
|
||||
int get_width_internal() override {
|
||||
if constexpr (HAS_HARDWARE_ROTATION)
|
||||
return get_width();
|
||||
return WIDTH;
|
||||
}
|
||||
int get_height_internal() override {
|
||||
if constexpr (HAS_HARDWARE_ROTATION)
|
||||
return get_height();
|
||||
return HEIGHT;
|
||||
}
|
||||
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
||||
|
||||
// reset the display, and write the init sequence
|
||||
@@ -166,9 +197,6 @@ class MipiSpi : public display::Display,
|
||||
case INVERT_ON:
|
||||
this->invert_colors_ = true;
|
||||
break;
|
||||
case MADCTL_CMD:
|
||||
this->madctl_ = arg_byte;
|
||||
break;
|
||||
case BRIGHTNESS:
|
||||
this->brightness_ = arg_byte;
|
||||
break;
|
||||
@@ -177,13 +205,13 @@ class MipiSpi : public display::Display,
|
||||
break;
|
||||
}
|
||||
const auto *ptr = vec.data() + index;
|
||||
esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
|
||||
this->write_command_(cmd, ptr, num_args);
|
||||
index += num_args;
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
this->reset_params_();
|
||||
// init sequence no longer needed
|
||||
this->init_sequence_.clear();
|
||||
}
|
||||
@@ -206,9 +234,10 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
|
||||
void dump_config() override {
|
||||
internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_,
|
||||
DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_,
|
||||
this->mode_, this->data_rate_, BUS_TYPE);
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
|
||||
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
|
||||
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||
HAS_HARDWARE_ROTATION);
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -219,10 +248,13 @@ class MipiSpi : public display::Display,
|
||||
|
||||
// Writes a command to the display, with the given bytes.
|
||||
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
|
||||
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
#endif
|
||||
// Don't spam the log after setup
|
||||
if (this->init_sequence_.empty()) {
|
||||
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
} else {
|
||||
esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
}
|
||||
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||
this->enable();
|
||||
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
||||
@@ -271,16 +303,60 @@ class MipiSpi : public display::Display,
|
||||
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
||||
if (this->brightness_.has_value())
|
||||
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
||||
|
||||
// calculate new madctl value from base value adjusted for rotation
|
||||
uint8_t madctl = MADCTL; // lower 8 bits only
|
||||
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
|
||||
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
|
||||
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
|
||||
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||
switch (this->rotation_) {
|
||||
default:
|
||||
break;
|
||||
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||
madctl ^= x_mask; // flip X axis
|
||||
madctl ^= MADCTL_MV; // swap X and Y axes
|
||||
break;
|
||||
case display::DISPLAY_ROTATION_180_DEGREES:
|
||||
madctl ^= x_mask; // flip X axis
|
||||
madctl ^= y_mask; // flip Y axis
|
||||
break;
|
||||
case display::DISPLAY_ROTATION_270_DEGREES:
|
||||
madctl ^= y_mask; // flip Y axis
|
||||
madctl ^= MADCTL_MV; // swap X and Y axes
|
||||
break;
|
||||
}
|
||||
}
|
||||
esph_log_d(TAG, "Setting MADCTL for rotation %d, value %X", this->rotation_, madctl);
|
||||
this->write_command_(MADCTL_CMD, madctl);
|
||||
}
|
||||
|
||||
uint16_t get_offset_width_() {
|
||||
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return OFFSET_HEIGHT;
|
||||
}
|
||||
return OFFSET_WIDTH;
|
||||
}
|
||||
|
||||
uint16_t get_offset_height_() {
|
||||
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return OFFSET_WIDTH;
|
||||
}
|
||||
return OFFSET_HEIGHT;
|
||||
}
|
||||
|
||||
// set the address window for the next data write
|
||||
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
|
||||
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
|
||||
uint8_t buf[4];
|
||||
x1 += OFFSET_WIDTH;
|
||||
x2 += OFFSET_WIDTH;
|
||||
y1 += OFFSET_HEIGHT;
|
||||
y2 += OFFSET_HEIGHT;
|
||||
x1 += get_offset_width_();
|
||||
x2 += get_offset_width_();
|
||||
y1 += get_offset_height_();
|
||||
y2 += get_offset_height_();
|
||||
put16_be(buf, y1);
|
||||
put16_be(buf + 2, y2);
|
||||
this->write_command_(RASET, buf, sizeof buf);
|
||||
@@ -408,7 +484,6 @@ class MipiSpi : public display::Display,
|
||||
optional<uint8_t> brightness_{};
|
||||
const char *model_{"Unknown"};
|
||||
std::vector<uint8_t> init_sequence_{};
|
||||
uint8_t madctl_{};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -427,22 +502,21 @@ class MipiSpi : public display::Display,
|
||||
* @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even)
|
||||
*/
|
||||
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION,
|
||||
int FRACTION, unsigned ROUNDING>
|
||||
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL,
|
||||
bool HAS_HARDWARE_ROTATION, int FRACTION, unsigned ROUNDING>
|
||||
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
|
||||
OFFSET_WIDTH, OFFSET_HEIGHT> {
|
||||
OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION> {
|
||||
public:
|
||||
// these values define the buffer size needed to write in accordance with the chip pixel alignment
|
||||
// requirements. If the required rounding does not divide the width and height, we round up to the next multiple and
|
||||
// ignore the extra columns and rows when drawing, but use them to write to the display.
|
||||
static constexpr unsigned BUFFER_WIDTH = (WIDTH + ROUNDING - 1) / ROUNDING * ROUNDING;
|
||||
static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING;
|
||||
static constexpr size_t round_buffer(size_t size) { return (size + ROUNDING - 1) / ROUNDING * ROUNDING; }
|
||||
|
||||
MipiSpiBuffer() { this->rotation_ = ROTATION; }
|
||||
MipiSpiBuffer() = default;
|
||||
|
||||
void dump_config() override {
|
||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
||||
OFFSET_HEIGHT>::dump_config();
|
||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||
MADCTL, HAS_HARDWARE_ROTATION>::dump_config();
|
||||
esph_log_config(TAG,
|
||||
" Rotation: %d°\n"
|
||||
" Buffer pixels: %d bits\n"
|
||||
@@ -450,14 +524,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
" Buffer bytes: %zu\n"
|
||||
" Draw rounding: %u",
|
||||
this->rotation_, BUFFERPIXEL * 8, FRACTION,
|
||||
sizeof(BUFFERTYPE) * BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION, ROUNDING);
|
||||
sizeof(BUFFERTYPE) * round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION, ROUNDING);
|
||||
}
|
||||
|
||||
void setup() override {
|
||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
||||
OFFSET_HEIGHT>::setup();
|
||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||
MADCTL, HAS_HARDWARE_ROTATION>::setup();
|
||||
RAMAllocator<BUFFERTYPE> allocator{};
|
||||
this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION);
|
||||
this->buffer_ = allocator.allocate(round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION);
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed(LOG_STR("Buffer allocation failed"));
|
||||
}
|
||||
@@ -472,11 +546,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
}
|
||||
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
|
||||
// the display height,
|
||||
for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) {
|
||||
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
|
||||
this->start_line_ += this->get_height_internal() / FRACTION) {
|
||||
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||
auto lap = millis();
|
||||
#endif
|
||||
this->end_line_ = this->start_line_ + HEIGHT / FRACTION;
|
||||
this->end_line_ =
|
||||
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
|
||||
if (this->auto_clear_enabled_) {
|
||||
this->clear();
|
||||
}
|
||||
@@ -503,10 +579,10 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
int w = this->x_high_ - this->x_low_ + 1;
|
||||
int h = this->y_high_ - this->y_low_ + 1;
|
||||
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
||||
this->y_low_ - this->start_line_, BUFFER_WIDTH - w);
|
||||
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
|
||||
// invalidate watermarks
|
||||
this->x_low_ = WIDTH;
|
||||
this->y_low_ = HEIGHT;
|
||||
this->x_low_ = this->get_width_internal();
|
||||
this->y_low_ = this->get_height_internal();
|
||||
this->x_high_ = 0;
|
||||
this->y_high_ = 0;
|
||||
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||
@@ -523,10 +599,23 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
void draw_pixel_at(int x, int y, Color color) override {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
rotate_coordinates(x, y);
|
||||
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
|
||||
if constexpr (not HAS_HARDWARE_ROTATION) {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||
x = WIDTH - x - 1;
|
||||
y = HEIGHT - y - 1;
|
||||
} else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) {
|
||||
auto tmp = x;
|
||||
x = WIDTH - y - 1;
|
||||
y = tmp;
|
||||
} else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) {
|
||||
auto tmp = y;
|
||||
y = HEIGHT - x - 1;
|
||||
x = tmp;
|
||||
}
|
||||
}
|
||||
if (x < 0 || x >= this->get_width_internal() || y < this->start_line_ || y >= this->end_line_)
|
||||
return;
|
||||
this->buffer_[(y - this->start_line_) * BUFFER_WIDTH + x] = convert_color(color);
|
||||
this->buffer_[(y - this->start_line_) * round_buffer(this->get_width_internal()) + x] = convert_color(color);
|
||||
if (x < this->x_low_) {
|
||||
this->x_low_ = x;
|
||||
}
|
||||
@@ -551,39 +640,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = this->start_line_;
|
||||
this->x_high_ = WIDTH - 1;
|
||||
this->x_high_ = this->get_width_internal() - 1;
|
||||
this->y_high_ = this->end_line_ - 1;
|
||||
std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color));
|
||||
}
|
||||
|
||||
int get_width() override {
|
||||
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return HEIGHT;
|
||||
return WIDTH;
|
||||
}
|
||||
|
||||
int get_height() override {
|
||||
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return WIDTH;
|
||||
return HEIGHT;
|
||||
std::fill_n(this->buffer_, (this->end_line_ - this->start_line_) * round_buffer(this->get_width_internal()),
|
||||
convert_color(color));
|
||||
}
|
||||
|
||||
protected:
|
||||
// Rotate the coordinates to match the display orientation.
|
||||
static void rotate_coordinates(int &x, int &y) {
|
||||
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||
x = WIDTH - x - 1;
|
||||
y = HEIGHT - y - 1;
|
||||
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) {
|
||||
auto tmp = x;
|
||||
x = WIDTH - y - 1;
|
||||
y = tmp;
|
||||
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) {
|
||||
auto tmp = y;
|
||||
y = HEIGHT - x - 1;
|
||||
x = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert a color to the buffer pixel format.
|
||||
static BUFFERTYPE convert_color(const Color &color) {
|
||||
|
||||
@@ -376,7 +376,7 @@ size_t ModbusController::create_register_ranges_() {
|
||||
while (ix != this->sensorset_.end()) {
|
||||
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);
|
||||
|
||||
if (r.register_count == 0) {
|
||||
@@ -484,18 +484,18 @@ void ModbusController::dump_config() {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGCONFIG(TAG, "sensormap");
|
||||
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,
|
||||
it->get_register_size());
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "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);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "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);
|
||||
}
|
||||
#endif
|
||||
@@ -524,7 +524,7 @@ void ModbusController::on_write_register_response(ModbusRegisterType register_ty
|
||||
void ModbusController::dump_sensors_() {
|
||||
ESP_LOGV(TAG, "sensors");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import esphome.codegen as cg
|
||||
|
||||
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
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
@@ -15,6 +17,7 @@ from esphome.const import (
|
||||
from . import st7735_ns
|
||||
|
||||
CODEOWNERS = ["@SenexCrenshaw"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
@@ -87,6 +90,9 @@ async def setup_st7735(var, 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(
|
||||
config[CONF_ID],
|
||||
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] &&
|
||||
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();
|
||||
if (!time.is_valid())
|
||||
return;
|
||||
|
||||
@@ -26,10 +26,11 @@ class CronTrigger : public Trigger<>, public Component {
|
||||
void add_day_of_week(uint8_t day_of_week);
|
||||
void add_days_of_week(const std::vector<uint8_t> &days_of_week);
|
||||
bool matches(const ESPTime &time);
|
||||
void loop() override;
|
||||
void setup() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
void check_time_();
|
||||
std::bitset<61> seconds_;
|
||||
std::bitset<60> minutes_;
|
||||
std::bitset<24> hours_;
|
||||
|
||||
@@ -70,7 +70,7 @@ void UptimeTextSensor::update() {
|
||||
if (show_seconds)
|
||||
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; }
|
||||
|
||||
@@ -286,10 +286,11 @@ void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char
|
||||
this->send(message, event, id, reconnect);
|
||||
}
|
||||
|
||||
void DeferredUpdateEventSourceList::loop() {
|
||||
bool DeferredUpdateEventSourceList::loop() {
|
||||
for (DeferredUpdateEventSource *dues : *this) {
|
||||
dues->loop();
|
||||
}
|
||||
return !this->empty();
|
||||
}
|
||||
|
||||
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->handleRequest(request);
|
||||
ws->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
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
|
||||
// getting a lot of events
|
||||
this->set_interval(10000, [this]() {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
char buf[32];
|
||||
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
|
||||
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
|
||||
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
|
||||
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);
|
||||
|
||||
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 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->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
|
||||
// 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
|
||||
@@ -504,6 +507,7 @@ void AsyncEventSource::loop() {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
return !this->sessions_.empty();
|
||||
}
|
||||
|
||||
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 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; }
|
||||
|
||||
size_t count() const { return this->sessions_.size(); }
|
||||
|
||||
+2
-2
@@ -10,9 +10,9 @@ tzdata>=2021.1 # from time
|
||||
pyserial==3.5
|
||||
platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.3.1
|
||||
click==8.3.2
|
||||
esphome-dashboard==20260210.0
|
||||
aioesphomeapi==44.8.1
|
||||
aioesphomeapi==44.9.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
|
||||
@@ -44,7 +44,8 @@ def find_and_activate_virtualenv():
|
||||
def run_command():
|
||||
# Execute the remaining arguments in the new environment
|
||||
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:
|
||||
print(
|
||||
"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