mirror of
https://github.com/esphome/esphome.git
synced 2026-09-18 18:48:39 +00:00
Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
This commit is contained in:
Symlink
+1
@@ -0,0 +1 @@
|
||||
../.agents/skills
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../.agents/skills
|
||||
@@ -265,6 +265,7 @@ esphome/components/i2s_audio/* @jesserockz
|
||||
esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
esphome/components/icnt86/* @danepowell
|
||||
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
||||
esphome/components/improv_base/* @esphome/core
|
||||
esphome/components/improv_ble/* @jesserockz
|
||||
@@ -477,6 +478,7 @@ esphome/components/sendspin/image/* @kahrendt
|
||||
esphome/components/sendspin/media_player/* @kahrendt
|
||||
esphome/components/sendspin/media_source/* @kahrendt
|
||||
esphome/components/sendspin/sensor/* @kahrendt
|
||||
esphome/components/sendspin/switch/* @kahrendt
|
||||
esphome/components/sendspin/text_sensor/* @kahrendt
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
|
||||
@@ -90,9 +90,10 @@ def get_project_cmakelists(
|
||||
"""
|
||||
idf_target = variant_to_idf_target(get_esp32_variant())
|
||||
|
||||
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
|
||||
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
|
||||
# --format=raw because the legacy mode doesn't support it.
|
||||
# esp_idf_size 2.x (IDF >=6.0) made NG the default and removed --ng;
|
||||
# 1.x (IDF 5.5) needs --ng for --format=json2. 1.x json2 also lacks
|
||||
# total_size, hence the ELF fallback in espidf/size_summary.py; both
|
||||
# go away together when 1.x support is dropped.
|
||||
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
|
||||
|
||||
# Project-wide compile options: -D defines and -W warning flags (skip
|
||||
@@ -211,10 +212,12 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
|
||||
project({CORE.name})
|
||||
|
||||
# Emit raw JSON size data for ESPHome to read post-build.
|
||||
# Emit per-memory-type JSON size data for ESPHome to read post-build.
|
||||
# json2 stays small; raw dumps every symbol (~2s on a large map) and
|
||||
# this command runs inside the link edge, blocking everything downstream.
|
||||
add_custom_command(
|
||||
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
|
||||
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
|
||||
${{CMAKE_PROJECT_NAME}}.map
|
||||
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
|
||||
|
||||
@@ -720,12 +720,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
|
||||
}
|
||||
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
|
||||
|
||||
if (msg.state) {
|
||||
a_switch->turn_on();
|
||||
} else {
|
||||
a_switch->turn_off();
|
||||
}
|
||||
a_switch->control(msg.state);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -339,7 +339,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# HTTPS streams verify the server against the root certificate bundle
|
||||
require_certificate_bundle()
|
||||
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.0")
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
|
||||
|
||||
data = _get_data()
|
||||
|
||||
|
||||
@@ -17,11 +17,7 @@ class BinaryLightOutput final : public light::LightOutput {
|
||||
void write_state(light::LightState *state) override {
|
||||
bool binary;
|
||||
state->current_values_as_binary(&binary);
|
||||
if (binary) {
|
||||
this->output_->turn_on();
|
||||
} else {
|
||||
this->output_->turn_off();
|
||||
}
|
||||
this->output_->set_state(binary);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -626,7 +626,7 @@ void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) {
|
||||
// explicit kick the MTU would only be exchanged on the first GATT query,
|
||||
// which never happens on a V3_WITH_CACHE connection.
|
||||
// Both registration calls above return void (BTstack 075a078, arduino-pico
|
||||
// 6.0.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
|
||||
// 6.1.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
|
||||
// the connect timeout in loop().
|
||||
gatt_client_send_mtu_negotiation(&RP2GattClient::gatt_packet_handler, this->con_handle_);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,6 @@ void CopySwitch::setup() {
|
||||
|
||||
void CopySwitch::dump_config() { LOG_SWITCH("", "Copy Switch", this); }
|
||||
|
||||
void CopySwitch::write_state(bool state) {
|
||||
if (state) {
|
||||
source_->turn_on();
|
||||
} else {
|
||||
source_->turn_off();
|
||||
}
|
||||
}
|
||||
void CopySwitch::write_state(bool state) { this->source_->control(state); }
|
||||
|
||||
} // namespace esphome::copy
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <utility>
|
||||
#include <numbers>
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -770,10 +771,12 @@ Rect Display::get_clipping() const {
|
||||
|
||||
void Display::clear_clipping_() { this->clipping_rectangle_.clear(); }
|
||||
|
||||
void Display::feed_wdt_pixel_slow_() { App.feed_wdt(); }
|
||||
|
||||
bool Display::clip(int x, int y) {
|
||||
if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height())
|
||||
return false;
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
if (this->is_point_clipped(x, y))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -758,6 +758,13 @@ class Display : public PollingComponent {
|
||||
|
||||
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
|
||||
|
||||
/// Whether (x, y) falls outside the active clipping rectangle. Tests the
|
||||
/// stack top in place: get_clipping() is out of line and returns the Rect
|
||||
/// by value, which per pixel drawing cannot afford.
|
||||
bool ESPHOME_ALWAYS_INLINE is_point_clipped(int x, int y) const {
|
||||
return this->is_clipping() && !this->clipping_rectangle_.back().inside(x, y);
|
||||
}
|
||||
|
||||
/** Check if pixel is within region of display.
|
||||
*/
|
||||
bool clip(int x, int y);
|
||||
@@ -774,6 +781,17 @@ class Display : public PollingComponent {
|
||||
void do_update_();
|
||||
void clear_clipping_();
|
||||
|
||||
/// Watchdog feed for per pixel loops. App.feed_wdt() is already rate
|
||||
/// limited, but every call reads the clock; only every 256th pixel makes
|
||||
/// that call, so the real feeds are unchanged and a pixel costs a counter.
|
||||
/// At 20 us per pixel on the slowest e-paper path that is about 5 ms
|
||||
/// between clock reads.
|
||||
void ESPHOME_ALWAYS_INLINE feed_wdt_per_pixel_() {
|
||||
if (++this->wdt_pixel_counter_ == 0)
|
||||
this->feed_wdt_pixel_slow_();
|
||||
}
|
||||
void feed_wdt_pixel_slow_();
|
||||
|
||||
virtual int get_height_internal() = 0;
|
||||
virtual int get_width_internal() = 0;
|
||||
|
||||
@@ -793,6 +811,7 @@ class Display : public PollingComponent {
|
||||
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
|
||||
bool auto_clear_enabled_{true};
|
||||
std::vector<Rect> clipping_rectangle_;
|
||||
uint8_t wdt_pixel_counter_{0};
|
||||
bool show_test_card_{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::display {
|
||||
@@ -44,7 +43,7 @@ int DisplayBuffer::get_height() {
|
||||
}
|
||||
|
||||
void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
if (this->is_point_clipped(x, y))
|
||||
return; // NOLINT
|
||||
|
||||
switch (this->rotation_) {
|
||||
@@ -64,7 +63,7 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
||||
break;
|
||||
}
|
||||
this->draw_absolute_pixel_internal(x, y, color);
|
||||
App.feed_wdt();
|
||||
this->feed_wdt_per_pixel_();
|
||||
}
|
||||
|
||||
} // namespace esphome::display
|
||||
|
||||
@@ -63,16 +63,6 @@ bool Rect::equal(Rect rect) const {
|
||||
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
|
||||
}
|
||||
|
||||
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOLINT
|
||||
if (!this->is_set()) {
|
||||
return true;
|
||||
}
|
||||
if (absolute) {
|
||||
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
|
||||
}
|
||||
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
|
||||
}
|
||||
|
||||
bool Rect::inside(Rect rect) const {
|
||||
if (!this->is_set() || !rect.is_set()) {
|
||||
return true;
|
||||
|
||||
@@ -26,7 +26,15 @@ class Rect {
|
||||
void shrink(Rect rect);
|
||||
|
||||
bool inside(Rect rect) const;
|
||||
bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const;
|
||||
bool ESPHOME_ALWAYS_INLINE inside(int16_t test_x, int16_t test_y, bool absolute = true) const {
|
||||
if (!this->is_set()) {
|
||||
return true;
|
||||
}
|
||||
if (absolute) {
|
||||
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
|
||||
}
|
||||
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
|
||||
}
|
||||
bool equal(Rect rect) const;
|
||||
void info(const std::string &prefix = "rect info:");
|
||||
};
|
||||
|
||||
@@ -299,7 +299,7 @@ bool EPaperBase::initialise(bool partial) {
|
||||
* @return false if the coordinates are out of bounds
|
||||
*/
|
||||
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
if (this->is_point_clipped(x, y))
|
||||
return false;
|
||||
if (this->effective_transform_ & SWAP_XY)
|
||||
std::swap(x, y);
|
||||
|
||||
@@ -111,6 +111,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
|
||||
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
|
||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_FLASH_CHIP = "flash_chip"
|
||||
CONF_KEY_ID = "key_id"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
CONF_NVS_ENCRYPTION = "nvs_encryption"
|
||||
@@ -464,6 +465,20 @@ ESP32_CHIP_REVISIONS = {
|
||||
"3.1": "CONFIG_ESP32_REV_MIN_3_1",
|
||||
}
|
||||
|
||||
# Flash vendor drivers ESP-IDF can link; each costs IRAM plus a 124 B table in DRAM
|
||||
# and only the one matching the flash ID is ever used
|
||||
ESP32_FLASH_CHIPS = {
|
||||
"gd": "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP",
|
||||
"issi": "CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP",
|
||||
"mxic": "CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP",
|
||||
"winbond": "CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP",
|
||||
"boya": "CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP",
|
||||
"th": "CONFIG_SPI_FLASH_SUPPORT_TH_CHIP",
|
||||
"mxic_opi": "CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP",
|
||||
}
|
||||
FLASH_CHIP_GENERIC = "generic"
|
||||
FLASH_CHIP_OPI = "mxic_opi" # the octal driver, ESP32-S3 only
|
||||
|
||||
# Socket limit configuration for ESP-IDF
|
||||
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
|
||||
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
|
||||
@@ -1519,6 +1534,13 @@ def final_validate(config) -> None:
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION],
|
||||
)
|
||||
)
|
||||
if config[CONF_VARIANT] != VARIANT_ESP32S3 and config.get(CONF_FLASH_MODE) == "opi":
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_FLASH_MODE}: opi' is only supported on {VARIANT_ESP32S3}",
|
||||
path=[CONF_FLASH_MODE],
|
||||
)
|
||||
)
|
||||
if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
@@ -1526,6 +1548,25 @@ def final_validate(config) -> None:
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM],
|
||||
)
|
||||
)
|
||||
if (flash_chip := advanced.get(CONF_FLASH_CHIP)) is not None:
|
||||
opi = flash_chip == FLASH_CHIP_OPI
|
||||
if opi and config[CONF_VARIANT] != VARIANT_ESP32S3:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_FLASH_CHIP}: {flash_chip}' is only supported on {VARIANT_ESP32S3}",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
|
||||
)
|
||||
)
|
||||
elif opi != (config.get(CONF_FLASH_MODE) == "opi"):
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_FLASH_CHIP}: {flash_chip}' requires '{CONF_FLASH_MODE}: opi'"
|
||||
if opi
|
||||
else f"'{CONF_FLASH_CHIP}: {flash_chip}' does not match "
|
||||
f"'{CONF_FLASH_MODE}: opi'; octal flash uses {FLASH_CHIP_OPI}",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
|
||||
)
|
||||
)
|
||||
if (
|
||||
config[CONF_VARIANT] != VARIANT_ESP32P4
|
||||
and config.get(CONF_ENGINEERING_SAMPLE) is not None
|
||||
@@ -1964,6 +2005,9 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
*ESP32_CHIP_REVISIONS, string=True
|
||||
),
|
||||
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_FLASH_CHIP): cv.one_of(
|
||||
FLASH_CHIP_GENERIC, *ESP32_FLASH_CHIPS, lower=True
|
||||
),
|
||||
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
|
||||
# it will handle disabling DHCP server when AP is not configured.
|
||||
# Default to false (disabled) when WiFi is not used.
|
||||
@@ -2609,6 +2653,13 @@ async def to_code(config):
|
||||
# NVS finds stored preferences by key, so preference key migration is possible
|
||||
cg.add_define("USE_PREFERENCE_KEY_LOOKUP")
|
||||
cg.add_build_flag("-Wl,-z,noexecstack")
|
||||
# assert(), HAL_ASSERT and ESP_ERROR_CHECK bake __FILE__ into rodata, and
|
||||
# IDF's noflash placement puts the flash driver's copies in DRAM. The
|
||||
# basename keeps the panic output useful at a fraction of the size.
|
||||
# __FILE_NAME__ is a GCC 12 builtin; IDF 5.0 still ships GCC 11.2.
|
||||
if idf_version() >= cv.Version(5, 1, 0):
|
||||
cg.add_build_flag("-D__FILE__=__FILE_NAME__")
|
||||
cg.add_build_flag("-Wno-builtin-macro-redefined")
|
||||
# Deferred so KEY_COMPONENTS is fully populated -- see the coroutine.
|
||||
CORE.add_job(_finalize_arduino_aware_flags)
|
||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||
@@ -2725,6 +2776,8 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option(
|
||||
f"CONFIG_ESPTOOLPY_FLASHMODE_{flash_mode.upper()}", True
|
||||
)
|
||||
# the opi mode choice only exists once octal flash is enabled
|
||||
add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_OCT_FLASH", flash_mode == "opi")
|
||||
if flash_frequency := config.get(CONF_FLASH_FREQUENCY):
|
||||
add_idf_sdkconfig_option(
|
||||
f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True
|
||||
@@ -2749,6 +2802,11 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option(flag, rev == min_rev)
|
||||
cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET")
|
||||
|
||||
# Keep only the flash vendor driver the board needs; the boot log names it
|
||||
if (flash_chip := conf[CONF_ADVANCED].get(CONF_FLASH_CHIP)) is not None:
|
||||
for chip, flag in ESP32_FLASH_CHIPS.items():
|
||||
add_idf_sdkconfig_option(flag, chip == flash_chip)
|
||||
|
||||
# Use SRAM1 region as IRAM on ESP32 (original) variant
|
||||
# This provides an additional 40KB of IRAM by using SRAM1 memory that was previously
|
||||
# reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later.
|
||||
|
||||
@@ -173,7 +173,10 @@ static const char *const TAG = "esp32.crash";
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME);
|
||||
|
||||
void crash_handler_read_and_clear() {
|
||||
// Validate the NOINIT record. Runs on every has_data() call; re-running is
|
||||
// harmless and the magic is left alone so the record survives an OTA
|
||||
// rollback reboot, crash_handler_clear() drops it once an API client has it.
|
||||
static void read_crash_data() {
|
||||
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
|
||||
s_crash_data_valid = true;
|
||||
// Clamp counts to prevent out-of-bounds reads from corrupt .noinit data
|
||||
@@ -194,11 +197,12 @@ void crash_handler_read_and_clear() {
|
||||
s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count;
|
||||
#endif
|
||||
}
|
||||
// Don't clear magic here — crash data must survive OTA rollback reboots.
|
||||
// Magic is cleared by crash_handler_clear() after an API client receives the data.
|
||||
}
|
||||
|
||||
bool crash_handler_has_data() { return s_crash_data_valid; }
|
||||
bool crash_handler_has_data() {
|
||||
read_crash_data();
|
||||
return s_crash_data_valid;
|
||||
}
|
||||
|
||||
void crash_handler_clear() {
|
||||
// Only clear the magic so data doesn't survive the next reboot.
|
||||
@@ -426,7 +430,7 @@ static void log_foreign_addresses() {
|
||||
// crashes again during boot, and allowing the CLI's process_stacktrace to match
|
||||
// and decode each address individually.
|
||||
void crash_handler_log() {
|
||||
if (!s_crash_data_valid)
|
||||
if (!crash_handler_has_data())
|
||||
return;
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
/// Read and validate crash data from NOINIT memory.
|
||||
/// Does not clear the magic marker — call crash_handler_clear() after
|
||||
/// the data has been delivered to an API client so it survives OTA rollback reboots.
|
||||
void crash_handler_read_and_clear();
|
||||
|
||||
/// Log crash data if a crash was detected on previous boot.
|
||||
void crash_handler_log();
|
||||
|
||||
@@ -16,7 +11,7 @@ void crash_handler_log();
|
||||
/// Call after the data has been delivered to an API client.
|
||||
void crash_handler_clear();
|
||||
|
||||
/// Returns true if crash data was found this boot.
|
||||
/// Returns true if crash data was found this boot, reading it first if needed.
|
||||
bool crash_handler_has_data();
|
||||
|
||||
} // namespace esphome::esp32
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
// defines.h must come before crash_handler.h so USE_ESP32_CRASH_HANDLER is set
|
||||
// before crash_handler.h's #ifdef-guarded namespace block is parsed.
|
||||
#include "esphome/core/defines.h"
|
||||
#include "crash_handler.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <esp_clk_tree.h>
|
||||
@@ -45,11 +42,6 @@ void arch_restart() {
|
||||
}
|
||||
|
||||
void arch_init() {
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
// Read crash data from previous boot before anything else
|
||||
esp32::crash_handler_read_and_clear();
|
||||
#endif
|
||||
|
||||
// Enable the task watchdog only on the loop task (from which we're currently running)
|
||||
esp_task_wdt_add(nullptr);
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ void Esp32HostedUpdate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Mode: HTTP\n"
|
||||
" Source URL: %s",
|
||||
this->source_url_.c_str());
|
||||
this->source_url_);
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Mode: Embedded\n"
|
||||
@@ -215,7 +215,7 @@ bool Esp32HostedUpdate::fetch_manifest_() {
|
||||
|
||||
auto container = this->http_request_parent_->get(this->source_url_);
|
||||
if (container == nullptr || container->status_code != 200) {
|
||||
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str());
|
||||
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_);
|
||||
this->status_set_error(LOG_STR("Failed to fetch manifest"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// HTTP mode setters
|
||||
void set_source_url(const std::string &url) { this->source_url_ = url; }
|
||||
void set_source_url(const char *url) { this->source_url_ = url; }
|
||||
void set_http_request_parent(http_request::HttpRequestComponent *parent) { this->http_request_parent_ = parent; }
|
||||
#else
|
||||
// Embedded mode setters
|
||||
@@ -38,7 +38,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// HTTP mode members
|
||||
http_request::HttpRequestComponent *http_request_parent_{nullptr};
|
||||
std::string source_url_;
|
||||
const char *source_url_{nullptr}; // literal from codegen
|
||||
std::string firmware_url_;
|
||||
|
||||
// HTTP mode helpers
|
||||
|
||||
@@ -367,14 +367,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config.get(CONF_ENABLE_SERIAL1):
|
||||
enable_serial1()
|
||||
|
||||
# Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when
|
||||
# out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make
|
||||
# new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of
|
||||
# a NULL pointer (so the stacktrace makes more sense), and for consistency with Arduino 3,
|
||||
# which always aborts if exceptions are disabled.
|
||||
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
|
||||
cg.add_build_flag("-DNEW_OOM_ABORT")
|
||||
|
||||
# Force-include inline std::__throw_* overrides so GCC dead-strips the unused
|
||||
# libstdc++ error message strings (e.g. "basic_string::_M_create") from DRAM.
|
||||
# See throw_stubs.h for details. Must be prepended before <string>, so this
|
||||
|
||||
@@ -166,9 +166,17 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
|
||||
CONF_PASSWORD,
|
||||
)
|
||||
# web_server and prometheus keep the shared listener up; the captive
|
||||
# portal's copy only exists on the fallback AP and is the recovery path
|
||||
# portal's copy only exists on the fallback AP and is the recovery path.
|
||||
# web_server `ota: false` gates /update behind the captive portal on
|
||||
# every listener
|
||||
web_server_conf = full_conf.get(CONF_WEB_SERVER)
|
||||
plaintext_update_reachable = (
|
||||
web_server_conf.get(CONF_OTA) is not False
|
||||
if web_server_conf is not None
|
||||
else "prometheus" in full_conf
|
||||
)
|
||||
if (
|
||||
(CONF_WEB_SERVER in full_conf or "prometheus" in full_conf)
|
||||
plaintext_update_reachable
|
||||
and any(conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf)
|
||||
and any(
|
||||
CONF_ENCRYPTION in conf
|
||||
|
||||
@@ -13,18 +13,10 @@ void GPIOSwitch::setup() {
|
||||
bool initial_state = this->get_initial_state_with_restore_mode().value_or(false);
|
||||
|
||||
// write state before setup
|
||||
if (initial_state) {
|
||||
this->turn_on();
|
||||
} else {
|
||||
this->turn_off();
|
||||
}
|
||||
this->control(initial_state);
|
||||
this->pin_->setup();
|
||||
// write after setup again for other IOs
|
||||
if (initial_state) {
|
||||
this->turn_on();
|
||||
} else {
|
||||
this->turn_off();
|
||||
}
|
||||
this->control(initial_state);
|
||||
}
|
||||
void GPIOSwitch::dump_config() {
|
||||
LOG_SWITCH("", "GPIO Switch", this);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "hub75_component.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -124,11 +123,11 @@ void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) {
|
||||
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]]
|
||||
return;
|
||||
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
if (this->is_point_clipped(x, y))
|
||||
return;
|
||||
|
||||
driver_->set_pixel(x, y, color.r, color.g, color.b);
|
||||
App.feed_wdt();
|
||||
this->feed_wdt_per_pixel_();
|
||||
}
|
||||
|
||||
void HOT HUB75Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
|
||||
@@ -48,10 +48,11 @@ static esp_err_t spdif_write_cb(void *user_ctx, uint32_t *data, size_t size, Tic
|
||||
auto *speaker = static_cast<I2SAudioSpeakerSPDIF *>(user_ctx);
|
||||
size_t bytes_written = 0;
|
||||
esp_err_t err = i2s_channel_write(speaker->get_tx_handle(), data, size, &bytes_written, ticks_to_wait);
|
||||
if (err != ESP_OK) {
|
||||
if (err != ESP_OK || bytes_written != size) {
|
||||
ESP_LOGV(TAG, "I2S write failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size);
|
||||
return (err != ESP_OK) ? err : ESP_FAIL;
|
||||
}
|
||||
return err;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerSPDIF::setup() {
|
||||
@@ -167,33 +168,44 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!successful_setup) {
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
} else {
|
||||
// Preload DMA buffers with SPDIF-encoded silence before enabling the channel.
|
||||
// This ensures the first data transmitted is valid SPDIF (not raw zeros from
|
||||
// auto_clear) and prevents phantom DMA events before real audio is available.
|
||||
// Each preloaded block pushes a 0-real-frame record so that the corresponding
|
||||
// on_sent events drain in lockstep without crediting any audio frames.
|
||||
// Preload DMA buffers with SPDIF-encoded silence before enabling the channel.
|
||||
// This ensures the first data transmitted is valid SPDIF (not raw zeros from
|
||||
// auto_clear) and prevents phantom DMA events before real audio is available.
|
||||
// Each preloaded block pushes a 0-real-frame record so that the corresponding
|
||||
// on_sent events drain in lockstep without crediting any audio frames. Runs with
|
||||
// the channel disabled: at startup and after a resync.
|
||||
auto preload_silence = [&]() -> bool {
|
||||
bool ok = true;
|
||||
this->spdif_encoder_->set_preload_mode(true);
|
||||
for (size_t i = 0; i < SPDIF_DMA_BUFFERS_COUNT; i++) {
|
||||
// i2s_channel_preload_data is non-blocking (returns immediately when the preload buffer fills), so no wait.
|
||||
esp_err_t preload_err = this->spdif_encoder_->flush_with_silence(0);
|
||||
if (preload_err != ESP_OK) {
|
||||
break; // DMA preload buffer full or error
|
||||
}
|
||||
const uint32_t silence_record = 0;
|
||||
xQueueSendToBack(this->write_records_queue_, &silence_record, 0);
|
||||
if ((this->spdif_encoder_->flush_with_silence(0) != ESP_OK) ||
|
||||
(xQueueSendToBack(this->write_records_queue_, &silence_record, 0) != pdTRUE)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->spdif_encoder_->set_preload_mode(false);
|
||||
this->spdif_encoder_->reset(); // Clean encoder state for the main loop
|
||||
return ok;
|
||||
};
|
||||
|
||||
// Now register the callback and enable the channel
|
||||
if (successful_setup) {
|
||||
successful_setup = preload_silence();
|
||||
}
|
||||
|
||||
if (successful_setup) {
|
||||
// Register the callback before enabling so the first transmitted block generates a queued event.
|
||||
xQueueReset(this->i2s_event_queue_);
|
||||
const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb};
|
||||
i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this);
|
||||
i2s_channel_enable(this->tx_handle_);
|
||||
successful_setup = i2s_channel_enable(this->tx_handle_) == ESP_OK;
|
||||
}
|
||||
|
||||
if (!successful_setup) {
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
} else {
|
||||
// Always-fill model: each iteration produces exactly one SPDIF block (= one DMA buffer).
|
||||
// We drain real PCM up to one block from the ring buffer and silence-pad any remainder.
|
||||
// Blocking writes pace the loop at the DMA consumption rate. This mirrors the standard
|
||||
@@ -210,24 +222,20 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
uint32_t spdif_pending_frames = 0;
|
||||
int64_t spdif_pending_timestamp = 0;
|
||||
uint32_t spdif_dma_event_count = 0;
|
||||
bool resync_needed = false;
|
||||
// Real frames consumed from the ring buffer that never reached a write record
|
||||
uint32_t unrecorded_frames = 0;
|
||||
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
|
||||
|
||||
// SPDIF continuous mode: loop runs indefinitely, outputting silence when no audio data
|
||||
// to keep the receiver synced. Exits only via break (stream info change, silence timeout,
|
||||
// lockstep desync, dropped event, or partial-write failure).
|
||||
// or a failed lockstep resync).
|
||||
while (true) {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
|
||||
|
||||
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
|
||||
// The ISR pairs COMMAND_STOP with ERR_DROPPED_EVENT when it has to discard a completion
|
||||
// event; that desyncs the lockstep queues permanently and the only safe recovery is a full
|
||||
// task restart.
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
|
||||
ESP_LOGV(TAG, "Exiting: ISR dropped event, restarting to recover lockstep");
|
||||
break;
|
||||
}
|
||||
// User-initiated stop. In SPDIF continuous mode, transition to silence output rather
|
||||
// than tearing the task down.
|
||||
this->spdif_silence_start_ = millis();
|
||||
@@ -244,6 +252,30 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
break;
|
||||
}
|
||||
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
|
||||
ESP_LOGE(TAG, "ISR event queue overflow, resyncing DMA lockstep");
|
||||
resync_needed = true;
|
||||
}
|
||||
if (resync_needed) {
|
||||
// Rebuild the lockstep in place. Frames held back by decimation are credited too, since their
|
||||
// blocks are discarded with the rest of the DMA contents.
|
||||
this->spdif_encoder_->reset();
|
||||
const uint32_t credited_frames = unrecorded_frames + spdif_pending_frames;
|
||||
const bool resynced = this->resync_lockstep_(credited_frames, preload_silence);
|
||||
unrecorded_frames = 0;
|
||||
spdif_pending_frames = 0;
|
||||
spdif_dma_event_count = 0;
|
||||
resync_needed = false;
|
||||
if (credited_frames > 0) {
|
||||
// Real audio was dropped, so the silence timer's start no longer reflects the stream
|
||||
this->spdif_silence_start_ = 0;
|
||||
}
|
||||
if (!resynced) {
|
||||
ESP_LOGE(TAG, "DMA lockstep resync failed, restarting speaker task");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Drain ISR completion events, popping a matching record for each.
|
||||
int64_t write_timestamp;
|
||||
bool lockstep_broken = false;
|
||||
@@ -253,8 +285,7 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
// order matches DMA completion order. Empty records queue here means lockstep broke.
|
||||
uint32_t real_frames = 0;
|
||||
if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) {
|
||||
ESP_LOGV(TAG, "Event without matching write record");
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
|
||||
ESP_LOGE(TAG, "Event without matching write record, resyncing DMA lockstep");
|
||||
lockstep_broken = true;
|
||||
break;
|
||||
}
|
||||
@@ -290,8 +321,8 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
}
|
||||
}
|
||||
if (lockstep_broken) {
|
||||
ESP_LOGV(TAG, "Exiting: lockstep desync, restarting task");
|
||||
break;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always-fill: produce exactly one SPDIF block this iteration. The blocking encoder write
|
||||
@@ -322,9 +353,8 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
&blocks_sent, &pcm_consumed);
|
||||
if (err != ESP_OK) {
|
||||
// A failed (or timed-out) send leaves an unsent block in the encoder's stitch buffer;
|
||||
// resuming would credit the next iteration's bytes against an old block. Bail and
|
||||
// let loop() restart the task with a clean encoder.
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
|
||||
// resuming would credit the next iteration's bytes against an old block.
|
||||
ESP_LOGE(TAG, "SPDIF block send failed, resyncing DMA lockstep");
|
||||
partial_write_failure = true;
|
||||
break;
|
||||
}
|
||||
@@ -341,7 +371,9 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
}
|
||||
|
||||
if (partial_write_failure) {
|
||||
break;
|
||||
unrecorded_frames += real_frames_in_block;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!block_committed) {
|
||||
@@ -349,16 +381,20 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
|
||||
// or emit a full silence block if the encoder is empty.
|
||||
esp_err_t err = this->spdif_encoder_->flush_with_silence(write_timeout_ticks);
|
||||
if (err != ESP_OK) {
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
|
||||
break;
|
||||
ESP_LOGE(TAG, "SPDIF block send failed, resyncing DMA lockstep");
|
||||
unrecorded_frames += real_frames_in_block;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// One block committed to DMA; push exactly one record carrying its real-audio frame count.
|
||||
// Failure here means the records queue is full, which violates the lockstep invariant.
|
||||
if (xQueueSendToBack(this->write_records_queue_, &real_frames_in_block, 0) != pdTRUE) {
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
|
||||
break;
|
||||
ESP_LOGE(TAG, "Write records queue full, resyncing DMA lockstep");
|
||||
unrecorded_frames += real_frames_in_block;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Silence-timeout tracking and graceful-stop reset.
|
||||
|
||||
@@ -80,17 +80,6 @@ void I2SAudioSpeakerBase::loop() {
|
||||
}
|
||||
if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) {
|
||||
ESP_LOGV(TAG, "Stopping");
|
||||
// Lockstep-breaking error bits are latched by the task and cleared along with all other bits
|
||||
// when TASK_STOPPED is processed; log them here, exactly once, as the task winds down.
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
|
||||
ESP_LOGE(TAG, "ISR event queue overflow, restarting speaker task to recover timestamp sync");
|
||||
}
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_PARTIAL_WRITE) {
|
||||
ESP_LOGE(TAG, "Partial DMA write broke buffer alignment, restarting speaker task");
|
||||
}
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC) {
|
||||
ESP_LOGE(TAG, "Event/record queues desynced, restarting speaker task");
|
||||
}
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
@@ -325,16 +314,10 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s
|
||||
I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) user_ctx;
|
||||
|
||||
if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) {
|
||||
// Queue is full, so discard the oldest event. Once we drop a completion event, ``i2s_event_queue_``
|
||||
// and any per-buffer record queue maintained by the task are permanently desynced, so the task
|
||||
// must restart to recover. Set both ERR_DROPPED_EVENT (so loop() can log it) and COMMAND_STOP
|
||||
// (so the task bails immediately, closing the race where loop() could clear the error bit
|
||||
// before the task observes it).
|
||||
// Queue is full, so discard the oldest event. The lockstep queues are now desynced; the task resyncs them.
|
||||
int64_t dummy;
|
||||
xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1);
|
||||
xEventGroupSetBitsFromISR(this_speaker->event_group_,
|
||||
SpeakerEventGroupBits::ERR_DROPPED_EVENT | SpeakerEventGroupBits::COMMAND_STOP,
|
||||
&need_yield2);
|
||||
xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT, &need_yield2);
|
||||
}
|
||||
|
||||
xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3);
|
||||
@@ -342,6 +325,24 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s
|
||||
return need_yield1 | need_yield2 | need_yield3;
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::drain_lockstep_(uint32_t extra_frames) {
|
||||
// Stop DMA so no more completion events arrive while the queues are rebuilt
|
||||
i2s_channel_disable(this->tx_handle_);
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT);
|
||||
|
||||
uint32_t frames = extra_frames;
|
||||
uint32_t record_frames = 0;
|
||||
while (xQueueReceive(this->write_records_queue_, &record_frames, 0) == pdTRUE) {
|
||||
frames += record_frames;
|
||||
}
|
||||
xQueueReset(this->i2s_event_queue_);
|
||||
|
||||
if (frames > 0) {
|
||||
ESP_LOGV(TAG, "Crediting %" PRIu32 " dropped frames as played", frames);
|
||||
this->audio_output_callback_(frames, esp_timer_get_time());
|
||||
}
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
|
||||
#ifdef USE_AUDIO_DAC
|
||||
if (this->audio_dac_ != nullptr) {
|
||||
|
||||
@@ -36,9 +36,7 @@ enum SpeakerEventGroupBits : uint32_t {
|
||||
|
||||
ERR_ESP_NO_MEM = (1 << 19),
|
||||
|
||||
ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event
|
||||
ERR_PARTIAL_WRITE = (1 << 21), // i2s_channel_write returned fewer bytes than requested
|
||||
ERR_LOCKSTEP_DESYNC = (1 << 22), // i2s_event_queue_ and write_records_queue_ fell out of sync
|
||||
ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event
|
||||
|
||||
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
|
||||
};
|
||||
@@ -134,6 +132,21 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
/// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup.
|
||||
virtual void on_task_stopped() {}
|
||||
|
||||
/// @brief Rebuilds the lockstep queues in place: disables the channel, credits every in-flight real frame as
|
||||
/// played now, empties both queues, preloads silence through ``preload`` and re-enables the channel. Speaker
|
||||
/// task only.
|
||||
/// @param extra_frames Real frames the caller consumed that never reached a write record
|
||||
/// @param preload Callable returning true once every DMA descriptor holds silence with a matching record
|
||||
/// @return false if the preload or the channel enable failed; the caller should restart the task
|
||||
template<typename F> bool resync_lockstep_(uint32_t extra_frames, F &&preload) {
|
||||
this->drain_lockstep_(extra_frames);
|
||||
return preload() && (i2s_channel_enable(this->tx_handle_) == ESP_OK);
|
||||
}
|
||||
|
||||
/// @brief Disables the channel, credits ``extra_frames`` plus every real frame still recorded as in flight,
|
||||
/// and empties both lockstep queues.
|
||||
void drain_lockstep_(uint32_t extra_frames);
|
||||
|
||||
/// @brief Apply software volume control by running the samples through the gain ramp. Called from the
|
||||
/// speaker task only.
|
||||
/// @param data Pointer to audio sample data (modified in place)
|
||||
|
||||
@@ -134,27 +134,29 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
}
|
||||
}
|
||||
|
||||
if (successful_setup) {
|
||||
// Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer.
|
||||
// This guarantees that every on_sent event has a corresponding write record from the start, so
|
||||
// ``i2s_event_queue_`` and ``write_records_queue_`` stay in lockstep for the entire task lifetime.
|
||||
// Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer, so every
|
||||
// on_sent event has a write record from the start. Runs with the channel disabled: at startup and after a resync.
|
||||
auto preload_silence = [&]() -> bool {
|
||||
for (size_t i = 0; i < DMA_BUFFERS_COUNT; i++) {
|
||||
size_t bytes_loaded = 0;
|
||||
esp_err_t err = i2s_channel_preload_data(this->tx_handle_, silence_buffer, dma_buffer_bytes, &bytes_loaded);
|
||||
if (err != ESP_OK || bytes_loaded != dma_buffer_bytes) {
|
||||
ESP_LOGV(TAG, "Failed to preload silence into DMA buffer %u (err=%d, loaded=%u)", (unsigned) i, (int) err,
|
||||
(unsigned) bytes_loaded);
|
||||
successful_setup = false;
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
uint32_t zero_real_frames = 0;
|
||||
if (xQueueSend(this->write_records_queue_, &zero_real_frames, 0) != pdTRUE) {
|
||||
// Should never happen: the queue was just reset and is sized for DMA_BUFFERS_COUNT * 2 entries.
|
||||
ESP_LOGV(TAG, "Failed to push preload write record");
|
||||
successful_setup = false;
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (successful_setup) {
|
||||
successful_setup = preload_silence();
|
||||
}
|
||||
|
||||
if (successful_setup) {
|
||||
@@ -177,6 +179,9 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
// stop to wait until every real-audio buffer has been confirmed played by an ISR event.
|
||||
uint32_t pending_real_buffers = 0;
|
||||
uint32_t last_data_received_time = millis();
|
||||
bool resync_needed = false;
|
||||
// Real frames consumed from the ring buffer that never reached a write record
|
||||
uint32_t unrecorded_frames = 0;
|
||||
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
|
||||
|
||||
@@ -197,8 +202,6 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
|
||||
|
||||
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
|
||||
// COMMAND_STOP is set both by user-initiated stop() and by the ISR when it drops a completion
|
||||
// event (paired with ERR_DROPPED_EVENT so loop() can distinguish the two cases).
|
||||
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
|
||||
ESP_LOGV(TAG, "Exiting: COMMAND_STOP received");
|
||||
break;
|
||||
@@ -214,6 +217,22 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
break;
|
||||
}
|
||||
|
||||
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
|
||||
ESP_LOGE(TAG, "ISR event queue overflow, resyncing DMA lockstep");
|
||||
resync_needed = true;
|
||||
}
|
||||
if (resync_needed) {
|
||||
// Rebuild the lockstep in place; the ring buffer keeps accepting audio throughout
|
||||
const bool resynced = this->resync_lockstep_(unrecorded_frames, preload_silence);
|
||||
unrecorded_frames = 0;
|
||||
pending_real_buffers = 0;
|
||||
resync_needed = false;
|
||||
if (!resynced) {
|
||||
ESP_LOGE(TAG, "DMA lockstep resync failed, restarting speaker task");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Drain ISR-stamped completion events. Each event corresponds 1:1 with a write_records_queue_
|
||||
// entry by construction (preloaded records at startup, plus exactly one record pushed per
|
||||
// iteration alongside exactly one DMA-buffer-sized write).
|
||||
@@ -223,8 +242,7 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
uint32_t real_frames = 0;
|
||||
if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) {
|
||||
// Should never happen: would indicate the lockstep invariant is broken.
|
||||
ESP_LOGV(TAG, "Event without matching write record");
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
|
||||
ESP_LOGE(TAG, "Event without matching write record, resyncing DMA lockstep");
|
||||
lockstep_broken = true;
|
||||
break;
|
||||
}
|
||||
@@ -240,7 +258,8 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
}
|
||||
}
|
||||
if (lockstep_broken) {
|
||||
break;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Graceful stop: exit only after the source's exposed chunk is drained, the underlying ring
|
||||
@@ -299,10 +318,12 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
size_t bw = 0;
|
||||
i2s_channel_write(this->tx_handle_, chunk, output_bytes, &bw, WRITE_TIMEOUT_TICKS);
|
||||
if (bw != output_bytes) {
|
||||
// A short real-audio write breaks DMA descriptor alignment for every subsequent event;
|
||||
// the only safe recovery is to restart the task.
|
||||
ESP_LOGV(TAG, "Partial real audio write: %u of %u bytes", (unsigned) bw, (unsigned) output_bytes);
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
|
||||
// A short write breaks DMA descriptor alignment for every subsequent event. Drop the chunk rather
|
||||
// than retry it: it was already narrowed in place.
|
||||
ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw,
|
||||
(unsigned) output_bytes);
|
||||
audio_source->consume(input_bytes);
|
||||
real_frames_total += frames_to_write;
|
||||
partial_write_failure = true;
|
||||
break;
|
||||
}
|
||||
@@ -316,7 +337,9 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
}
|
||||
|
||||
if (partial_write_failure) {
|
||||
break;
|
||||
unrecorded_frames += real_frames_total;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t silence_bytes = dma_buffer_bytes - bytes_written_total;
|
||||
@@ -325,19 +348,22 @@ void I2SAudioSpeaker::run_speaker_task() {
|
||||
i2s_channel_write(this->tx_handle_, silence_buffer, silence_bytes, &bw, WRITE_TIMEOUT_TICKS);
|
||||
if (bw != silence_bytes) {
|
||||
// Same descriptor-alignment hazard as a partial real-audio write.
|
||||
ESP_LOGV(TAG, "Partial silence write: %u of %u bytes", (unsigned) bw, (unsigned) silence_bytes);
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
|
||||
break;
|
||||
ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw,
|
||||
(unsigned) silence_bytes);
|
||||
unrecorded_frames += real_frames_total;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Push the matching write record. Capacity headroom in I2S_EVENT_QUEUE_COUNT guarantees this
|
||||
// succeeds even with a transient backlog of unprocessed events; if it ever fails the lockstep
|
||||
// invariant is broken and every subsequent timestamp would be silently wrong, so bail.
|
||||
// invariant is broken and every subsequent timestamp would be silently wrong, so rebuild it.
|
||||
if (xQueueSend(this->write_records_queue_, &real_frames_total, 0) != pdTRUE) {
|
||||
ESP_LOGV(TAG, "Exiting: write records queue full");
|
||||
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
|
||||
break;
|
||||
ESP_LOGE(TAG, "Write records queue full, resyncing DMA lockstep");
|
||||
unrecorded_frames += real_frames_total;
|
||||
resync_needed = true;
|
||||
continue;
|
||||
}
|
||||
if (real_frames_total > 0) {
|
||||
pending_real_buffers++;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@danepowell"]
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "icnt86.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::icnt86 {
|
||||
|
||||
static const char *const TAG = "icnt86";
|
||||
static constexpr uint16_t REG_TOUCH_NUM = 0x1001;
|
||||
static constexpr uint16_t REG_POINT1 = 0x1002;
|
||||
static constexpr uint8_t MAX_TOUCHES = 5;
|
||||
static constexpr uint8_t POINT_SIZE = 7;
|
||||
|
||||
void ICNT86Touchscreen::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up icnt86 Touchscreen...");
|
||||
|
||||
// Register interrupt pin
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
|
||||
// Perform reset if necessary
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
|
||||
if (this->x_raw_max_ == this->x_raw_min_) {
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
}
|
||||
if (this->y_raw_max_ == this->y_raw_min_) {
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
}
|
||||
|
||||
void ICNT86Touchscreen::update_touches() {
|
||||
uint8_t buf[MAX_TOUCHES * POINT_SIZE] = {0};
|
||||
uint8_t mask[1] = {0x00};
|
||||
|
||||
if (this->read_register16(REG_TOUCH_NUM, buf, 1) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
this->skip_update_ = true;
|
||||
ESP_LOGW(TAG, "Failed to read touch count");
|
||||
return;
|
||||
}
|
||||
uint8_t touch_count = buf[0];
|
||||
|
||||
if (touch_count == 0x00 || touch_count > MAX_TOUCHES) { // No new touch
|
||||
this->status_clear_warning();
|
||||
return;
|
||||
}
|
||||
if (this->read_register16(REG_POINT1, buf, touch_count * POINT_SIZE) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
this->skip_update_ = true;
|
||||
ESP_LOGW(TAG, "Failed to read touch points");
|
||||
return;
|
||||
}
|
||||
this->write_register16(REG_TOUCH_NUM, mask, 1);
|
||||
ESP_LOGV(TAG, "Touch count: %d", touch_count);
|
||||
this->status_clear_warning();
|
||||
|
||||
for (uint8_t i = 0; i < touch_count; i++) {
|
||||
uint16_t x = ((uint16_t) buf[2 + 7 * i] << 8) + buf[1 + 7 * i];
|
||||
uint16_t y = ((uint16_t) buf[4 + 7 * i] << 8) + buf[3 + 7 * i];
|
||||
uint8_t pressure = buf[5 + 7 * i];
|
||||
uint8_t touch_id = buf[6 + 7 * i];
|
||||
|
||||
// A zero-pressure report just means this point is no longer touched; skipping it here leaves is_touched_
|
||||
// false (when no other point is active) so send_touches_() reports the release as normal.
|
||||
if (pressure != 0) {
|
||||
this->add_raw_touch_position_(touch_id, x, y, pressure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ICNT86Touchscreen::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "icnt86 Touchscreen:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
}
|
||||
|
||||
} // namespace esphome::icnt86
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::icnt86 {
|
||||
|
||||
class ICNT86Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
void update_touches() override;
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::icnt86
|
||||
@@ -0,0 +1,40 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@danepowell"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
icnt86_ns = cg.esphome_ns.namespace("icnt86")
|
||||
ICNT86Touchscreen = icnt86_ns.class_(
|
||||
"ICNT86Touchscreen",
|
||||
touchscreen.Touchscreen,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = touchscreen.touchscreen_schema("250ms").extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ICNT86Touchscreen),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x48))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if interrupt_pin_config := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(
|
||||
var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin_config))
|
||||
)
|
||||
|
||||
if reset_pin_config := config.get(CONF_RESET_PIN):
|
||||
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin_config)))
|
||||
@@ -48,14 +48,12 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
continue; // skip drawing
|
||||
}
|
||||
break;
|
||||
case TRANSPARENCY_ALPHA_CHANNEL: {
|
||||
auto on = (float) gray / 255.0f;
|
||||
auto off = 1.0f - on;
|
||||
// blend color_on and color_off
|
||||
color = Color(color_on.r * on + color_off.r * off, color_on.g * on + color_off.g * off,
|
||||
color_on.b * on + color_off.b * off, 0xFF);
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
// gray is the alpha: blend from color_off to color_on, drawn opaque
|
||||
color = Color(Color::blend_channel(color_off.r, color_on.r, gray),
|
||||
Color::blend_channel(color_off.g, color_on.g, gray),
|
||||
Color::blend_channel(color_off.b, color_on.b, gray), 0xFF);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ class Image : public display::BaseImage {
|
||||
const uint8_t *data_start_;
|
||||
Transparency transparency_;
|
||||
size_t bpp_{};
|
||||
size_t stride_{};
|
||||
#ifdef USE_LVGL
|
||||
lv_img_dsc_t dsc_{};
|
||||
#endif
|
||||
|
||||
@@ -208,11 +208,7 @@ void ImprovBLEComponent::set_status_indicator_state_(bool state) {
|
||||
if (this->status_indicator_state_ == state)
|
||||
return;
|
||||
this->status_indicator_state_ = state;
|
||||
if (state) {
|
||||
this->status_indicator_->turn_on();
|
||||
} else {
|
||||
this->status_indicator_->turn_off();
|
||||
}
|
||||
this->status_indicator_->set_state(state);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -855,7 +855,7 @@ void IT8951Display::apply_transform_(int &x, int &y) const {
|
||||
}
|
||||
|
||||
bool IT8951Display::rotate_coordinates_(int &x, int &y) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
if (this->is_point_clipped(x, y))
|
||||
return false;
|
||||
this->apply_transform_(x, y);
|
||||
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
|
||||
@@ -929,7 +929,7 @@ void IT8951Display::fill(Color color) {
|
||||
void HOT IT8951Display::draw_pixel_at(int x, int y, Color color) {
|
||||
if (this->buffer_ == nullptr)
|
||||
return;
|
||||
App.feed_wdt();
|
||||
this->feed_wdt_per_pixel_();
|
||||
if (!this->rotate_coordinates_(x, y))
|
||||
return;
|
||||
this->write_pixel_native_(static_cast<uint16_t>(x), static_cast<uint16_t>(y), color);
|
||||
|
||||
@@ -66,6 +66,8 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape)
|
||||
}
|
||||
|
||||
JsonBuilder::JsonBuilder() = default;
|
||||
|
||||
SerializationBuffer<> JsonBuilder::serialize() {
|
||||
// ===========================================================================================
|
||||
// CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING
|
||||
|
||||
@@ -168,6 +168,9 @@ inline JsonDocument parse_json(const std::string &data) {
|
||||
/// Builder class for creating JSON documents without lambdas
|
||||
class JsonBuilder {
|
||||
public:
|
||||
// Out of line: inlining the JsonDocument constructor duplicates it at every call site
|
||||
JsonBuilder();
|
||||
|
||||
JsonObject root() {
|
||||
if (!root_created_) {
|
||||
root_ = doc_.to<JsonObject>();
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace esphome::ld2450 {
|
||||
|
||||
class MultiTargetSwitch : public switch_::Switch, public Parented<LD2450Component> {
|
||||
public:
|
||||
MultiTargetSwitch() = default;
|
||||
// User provided, not "= default": `new(p) MultiTargetSwitch()` would zero-fill .bss that is already zero.
|
||||
MultiTargetSwitch() {}
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
|
||||
@@ -301,14 +301,10 @@ void LD6002BComponent::setup() {
|
||||
target_display_controlled = true;
|
||||
// Nothing reports this switch back, so its restored state is the only state
|
||||
// there is. Restoring through the switch keeps its inversion in the path:
|
||||
// the restored value is logical, and turn_on()/turn_off() are what turn it
|
||||
// the restored value is logical, and driving the switch is what turns it
|
||||
// into the raw command, the published state and the stream flag.
|
||||
const bool state = this->target_display_switch_->get_initial_state_with_restore_mode().value_or(true);
|
||||
if (state) {
|
||||
this->target_display_switch_->turn_on();
|
||||
} else {
|
||||
this->target_display_switch_->turn_off();
|
||||
}
|
||||
this->target_display_switch_->control(state);
|
||||
}
|
||||
#endif
|
||||
if (!target_display_controlled) {
|
||||
@@ -328,11 +324,7 @@ void LD6002BComponent::setup() {
|
||||
// The switch owns the stream, so it is also what applies the restored state:
|
||||
// driving it rather than the module keeps the entity's inversion in the path.
|
||||
const bool state = this->point_cloud_switch_->get_initial_state_with_restore_mode().value_or(false);
|
||||
if (state) {
|
||||
this->point_cloud_switch_->turn_on();
|
||||
} else {
|
||||
this->point_cloud_switch_->turn_off();
|
||||
}
|
||||
this->point_cloud_switch_->control(state);
|
||||
}
|
||||
#endif
|
||||
if (!point_cloud_controlled) {
|
||||
@@ -375,11 +367,7 @@ void LD6002BComponent::setup() {
|
||||
// Driving the switch applies its inversion; it also marks the restored value
|
||||
// as reported, so the work mode fallback runs on that until the query lands.
|
||||
const bool state = this->low_power_switch_->get_initial_state_with_restore_mode().value_or(false);
|
||||
if (state) {
|
||||
this->low_power_switch_->turn_on();
|
||||
} else {
|
||||
this->low_power_switch_->turn_off();
|
||||
}
|
||||
this->low_power_switch_->control(state);
|
||||
}
|
||||
#else
|
||||
bool want_low_power = false;
|
||||
|
||||
@@ -38,14 +38,6 @@ float Mcp4461Wiper::update_state() {
|
||||
return this->state_;
|
||||
}
|
||||
|
||||
void Mcp4461Wiper::set_state(bool state) {
|
||||
if (state) {
|
||||
this->turn_on();
|
||||
} else {
|
||||
this->turn_off();
|
||||
}
|
||||
}
|
||||
|
||||
void Mcp4461Wiper::turn_on() { this->parent_->enable_wiper_(this->wiper_); }
|
||||
|
||||
void Mcp4461Wiper::turn_off() { this->parent_->disable_wiper_(this->wiper_); }
|
||||
|
||||
@@ -13,9 +13,6 @@ class Mcp4461Wiper final : public output::FloatOutput, public Parented<Mcp4461Co
|
||||
/// @brief Set level of wiper
|
||||
/// @param[in] state - The desired float level in range 0-1.0
|
||||
void set_level(float state);
|
||||
/// @brief Enables/Disables current output using bool parameter
|
||||
/// @param[in] state boolean var representing desired state (true=ON, false=OFF)
|
||||
void set_state(bool state) override;
|
||||
/// @brief Enables current output
|
||||
void turn_on() override;
|
||||
/// @brief Disables current output
|
||||
|
||||
@@ -221,6 +221,10 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
||||
sendspin_service.proto = MDNS_STR(SERVICE_TCP);
|
||||
sendspin_service.port = []() -> uint16_t { return USE_SENDSPIN_PORT; };
|
||||
sendspin_service.txt_records = {{MDNS_STR(TXT_SENDSPIN_PATH), MDNS_STR(VALUE_SENDSPIN_PATH)}};
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
// Starts disabled; the sendspin hub enables it once its server is running
|
||||
sendspin_service.enabled = false;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSERVER
|
||||
|
||||
@@ -259,7 +259,7 @@ bool MipiDsi::check_buffer_() {
|
||||
}
|
||||
|
||||
void MipiDsi::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
if (this->is_point_clipped(x, y))
|
||||
return;
|
||||
|
||||
switch (this->rotation_) {
|
||||
|
||||
@@ -259,7 +259,7 @@ bool MipiRgb::check_buffer_() {
|
||||
}
|
||||
|
||||
void MipiRgb::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y) || this->is_failed())
|
||||
if (this->is_point_clipped(x, y) || this->is_failed())
|
||||
return;
|
||||
|
||||
switch (this->rotation_) {
|
||||
|
||||
@@ -604,7 +604,7 @@ class MipiSpiBuffer
|
||||
|
||||
// Draw a pixel at the given coordinates.
|
||||
void draw_pixel_at(int x, int y, Color color) override {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
if (this->is_point_clipped(x, y))
|
||||
return;
|
||||
if constexpr (not HAS_HARDWARE_ROTATION) {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||
|
||||
@@ -155,7 +155,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SourceSpeaker),
|
||||
cv.Required(CONF_DECIBEL_REDUCTION): cv.templatable(
|
||||
cv.int_range(min=0, max=51)
|
||||
cv.int_range(min=0, max=255)
|
||||
),
|
||||
cv.Optional(CONF_DURATION, default="0.0s"): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
|
||||
@@ -385,7 +385,8 @@ void MixerSpeaker::loop() {
|
||||
// Retries on a subsequent loop if the task is still running on the other core
|
||||
if ((event_group_bits & MIXER_TASK_STATE_STOPPED) && this->task_.deallocate()) {
|
||||
ESP_LOGD(TAG, "Stopped");
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
|
||||
// Keep a start request that arrived while the task was stopping, otherwise it is lost for good
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS & ~MIXER_TASK_COMMAND_START);
|
||||
this->all_stopped_since_ms_ = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,7 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_CONTINUOUS,
|
||||
CONF_DISABLE_CRC,
|
||||
CONF_FLOW_CONTROL_PIN,
|
||||
CONF_ID,
|
||||
)
|
||||
from esphome.const import CONF_ADDRESS, CONF_CONTINUOUS, CONF_FLOW_CONTROL_PIN, CONF_ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_helpers import gpio_pin_expression
|
||||
import esphome.final_validate as fv
|
||||
@@ -285,10 +279,6 @@ CONFIG_SCHEMA = cv.typed_schema(
|
||||
cv.Optional(
|
||||
CONF_TURNAROUND_TIME, default="600ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
# Remove before 2026.10.0
|
||||
cv.Optional(CONF_DISABLE_CRC): cv.invalid(
|
||||
"'disable_crc' has been removed. The parser no longer requires it — remove this option."
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -297,10 +287,6 @@ CONFIG_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ModbusServer),
|
||||
cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema,
|
||||
# Remove before 2026.10.0
|
||||
cv.Optional(CONF_DISABLE_CRC): cv.invalid(
|
||||
"'disable_crc' has been removed. The parser no longer requires it — remove this option."
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@@ -252,14 +252,6 @@ class ModbusClientHub : public Modbus {
|
||||
void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_us_ = time_in_ms * 1000UL; }
|
||||
bool tx_buffer_empty();
|
||||
bool tx_blocked() override;
|
||||
ESPDEPRECATED("Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
|
||||
uint8_t payload_len = 0, const uint8_t *payload = nullptr, ModbusClientDevice *device = nullptr) {
|
||||
this->queue_pdu(address,
|
||||
helpers::create_client_pdu((FunctionCode) function_code, start_address, number_of_entities, payload,
|
||||
payload_len),
|
||||
device);
|
||||
};
|
||||
/// Queue a request. True = accepted: it resolves in exactly one terminal callback (a broadcast,
|
||||
/// address 0, gets only on_sent()). False = refused, and no callback of any kind follows.
|
||||
/// Neither means anything reached the wire - on_sent() reports that.
|
||||
|
||||
@@ -30,53 +30,10 @@ using modbus::ModbusFunctionCode;
|
||||
using modbus::ModbusRegisterType;
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
// Remove before 2026.10.0 — these helpers have moved to modbus::helpers
|
||||
ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); }
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline FunctionCode modbus_register_read_function(modbus::EntityType reg_type) {
|
||||
return modbus::helpers::modbus_register_read_function(reg_type);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline FunctionCode modbus_register_write_function(modbus::EntityType reg_type) {
|
||||
return modbus::helpers::modbus_register_write_function(reg_type);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::c_to_hex() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint8_t c_to_hex(char c) { return modbus::helpers::c_to_hex(c); }
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::byte_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::byte_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::word_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::word_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::dword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::dword_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::qword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::qword_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
|
||||
return modbus::helpers::get_data<T>(data, buffer_offset);
|
||||
}
|
||||
|
||||
// Span overloads of the deprecated helpers below: read lambdas receive their payload as a
|
||||
// Span overloads of the former modbus_controller helpers: read lambdas receive their payload as a
|
||||
// std::span<const uint8_t> (previously a const std::vector<uint8_t> &), and a span does not convert to
|
||||
// a vector, so existing lambdas calling these by name need an overload that accepts one. These carry
|
||||
// this release's deprecation window, since the span forms only exist from it.
|
||||
// the 2026.8.0 deprecation window, since the span forms only exist from it.
|
||||
// payload_to_number() deliberately has no such overload: one of its arguments is a modbus::helpers
|
||||
// type, so a span call already reaches the helper by argument-dependent lookup, and a forwarder here
|
||||
// would only make that call ambiguous.
|
||||
@@ -99,33 +56,6 @@ inline bool coil_from_vector(int coil, std::span<const uint8_t> data) {
|
||||
return modbus::helpers::bit_from_packed(coil, data);
|
||||
}
|
||||
|
||||
template<typename N>
|
||||
ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
N mask_and_shift_by_rightbit(N data, uint32_t mask) {
|
||||
return modbus::helpers::mask_and_shift_by_rightbit(data, mask);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
|
||||
modbus::helpers::number_to_payload(data, value, value_type);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
|
||||
uint32_t bitmask) {
|
||||
return modbus::helpers::payload_to_number(std::span<const uint8_t>(data), sensor_value_type, offset, bitmask)
|
||||
.value_or(0);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
|
||||
std::vector<uint16_t> data;
|
||||
modbus::helpers::float_to_payload(data, value, value_type);
|
||||
return data;
|
||||
}
|
||||
|
||||
class ModbusController;
|
||||
|
||||
/// How an item relates to the register range built just before it (same register type, address order).
|
||||
/// The numeric order doubles as the comparator tiebreak for items at the same address (see
|
||||
/// SensorItemsComparator): AUTO items form the shared range first, so a NEVER item comes last and
|
||||
|
||||
@@ -23,7 +23,6 @@ void ModbusNumber::parse_and_publish(std::span<const uint8_t> data) {
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Number new state : %.02f", result);
|
||||
// this->sensor_->raw_state = result;
|
||||
this->publish_state(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ void ModbusSensor::parse_and_publish(std::span<const uint8_t> data) {
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Sensor new state: %.02f", result);
|
||||
// this->sensor_->raw_state = result;
|
||||
this->publish_state(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,7 @@ void ModbusSwitch::setup() {
|
||||
optional<bool> initial_state = Switch::get_initial_state_with_restore_mode();
|
||||
if (initial_state.has_value()) {
|
||||
// if it has a value, restore_mode is not "DISABLED", therefore act on the switch:
|
||||
if (initial_state.value()) {
|
||||
this->turn_on();
|
||||
} else {
|
||||
this->turn_off();
|
||||
}
|
||||
this->control(initial_state.value());
|
||||
}
|
||||
}
|
||||
void ModbusSwitch::dump_config() { LOG_SWITCH(TAG, "Modbus Controller Switch", this); }
|
||||
|
||||
@@ -66,6 +66,7 @@ class NextionComponentBase {
|
||||
#ifdef USE_NEXTION_WAVEFORM
|
||||
uint8_t get_wave_channel_id() const { return this->wave_chan_id_; }
|
||||
void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; }
|
||||
void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; }
|
||||
|
||||
const std::vector<uint8_t> &get_wave_buffer() const { return this->wave_buffer_; }
|
||||
size_t get_wave_buffer_size() const { return this->wave_buffer_.size(); }
|
||||
@@ -86,12 +87,6 @@ class NextionComponentBase {
|
||||
virtual void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion){};
|
||||
virtual void send_state_to_nextion(){};
|
||||
bool get_needs_to_send_update() const { return this->needs_to_send_update_; }
|
||||
#ifdef USE_NEXTION_WAVEFORM
|
||||
// Remove before 2026.10.0
|
||||
ESPDEPRECATED("Use get_wave_channel_id() instead. Will be removed in 2026.10.0", "2026.4.0")
|
||||
uint8_t get_wave_chan_id() const { return this->get_wave_channel_id(); }
|
||||
void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; }
|
||||
#endif // USE_NEXTION_WAVEFORM
|
||||
|
||||
protected:
|
||||
std::string variable_name_;
|
||||
|
||||
@@ -6,21 +6,9 @@ namespace esphome::output {
|
||||
static const char *const TAG = "output.switch";
|
||||
|
||||
void OutputSwitch::dump_config() { LOG_SWITCH("", "Output Switch", this); }
|
||||
void OutputSwitch::setup() {
|
||||
bool initial_state = this->get_initial_state_with_restore_mode().value_or(false);
|
||||
|
||||
if (initial_state) {
|
||||
this->turn_on();
|
||||
} else {
|
||||
this->turn_off();
|
||||
}
|
||||
}
|
||||
void OutputSwitch::setup() { this->control(this->get_initial_state_with_restore_mode().value_or(false)); }
|
||||
void OutputSwitch::write_state(bool state) {
|
||||
if (state) {
|
||||
this->output_->turn_on();
|
||||
} else {
|
||||
this->output_->turn_off();
|
||||
}
|
||||
this->output_->set_state(state);
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ void Pixoo::set_pixel_(uint32_t index, Color color) {
|
||||
}
|
||||
|
||||
void HOT Pixoo::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
if (this->is_point_clipped(x, y))
|
||||
return;
|
||||
const int side = static_cast<int>(this->model_);
|
||||
switch (this->rotation_) {
|
||||
|
||||
@@ -169,6 +169,12 @@ def request_protocol(name: str) -> None:
|
||||
cg.add_define(protocol_define(name))
|
||||
|
||||
|
||||
def _request_protocol_if_in_tree(name: str) -> None:
|
||||
"""Registry names from external components have no source file here and need no define."""
|
||||
if _protocol_stem(name) in _PROTOCOL_STEMS:
|
||||
request_protocol(name)
|
||||
|
||||
|
||||
# Only the protocol sources a configuration uses are compiled
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
||||
{f"{stem}_protocol.cpp": protocol_define(stem) for stem in _PROTOCOL_STEMS}
|
||||
@@ -182,7 +188,7 @@ def register_binary_sensor(
|
||||
|
||||
def decorator(func: Callable[[MockObj, ConfigType], Any]) -> Callable:
|
||||
async def new_func(var: MockObj, config: ConfigType) -> None:
|
||||
request_protocol(name)
|
||||
_request_protocol_if_in_tree(name)
|
||||
await coroutine(func)(var, config)
|
||||
|
||||
return registerer(new_func)
|
||||
@@ -200,7 +206,7 @@ def register_trigger(name, type, data_type):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config):
|
||||
request_protocol(name)
|
||||
_request_protocol_if_in_tree(name)
|
||||
var = cg.new_Pvariable(config[CONF_TRIGGER_ID])
|
||||
await coroutine(func)(var, config)
|
||||
await automation.build_automation(var, [(data_type, "x")], config)
|
||||
@@ -218,7 +224,7 @@ def register_dumper(name, type, schema=None):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config, dumper_id):
|
||||
request_protocol(name)
|
||||
_request_protocol_if_in_tree(name)
|
||||
var = cg.new_Pvariable(dumper_id)
|
||||
await coroutine(func)(var, config)
|
||||
return var
|
||||
@@ -259,7 +265,7 @@ def register_action(name, type_, schema):
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config, action_id, template_arg, args):
|
||||
request_protocol(name)
|
||||
_request_protocol_if_in_tree(name)
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await register_transmittable(var, config)
|
||||
if CONF_REPEAT in config:
|
||||
|
||||
@@ -112,20 +112,21 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
|
||||
cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema),
|
||||
cv.Optional(CONF_DUMP, default=[]): remote_base.validate_dumpers,
|
||||
cv.Optional(CONF_TOLERANCE, default="25%"): validate_tolerance,
|
||||
# pulse ring targets hold one 4 byte entry per pulse; 4000b keeps their 1000 pulses
|
||||
cv.SplitDefault(
|
||||
CONF_BUFFER_SIZE,
|
||||
esp32=cv.UNDEFINED,
|
||||
# the pulse ring needs a size; only RMT targets size themselves in setup()
|
||||
**{
|
||||
f"esp32_{variant.removeprefix('ESP32').lower()}": "1000b"
|
||||
f"esp32_{variant.removeprefix('ESP32').lower()}": "4000b"
|
||||
for variant in esp32_rmt.VARIANTS_NO_RMT
|
||||
},
|
||||
esp8266="1000b",
|
||||
bk72xx="1000b",
|
||||
ln882x="1000b",
|
||||
rtl87xx="1000b",
|
||||
rp2="1000b",
|
||||
): cv.All(cv.validate_bytes, cv.int_range(min=64)),
|
||||
esp8266="4000b",
|
||||
bk72xx="4000b",
|
||||
ln882x="4000b",
|
||||
rtl87xx="4000b",
|
||||
rp2="4000b",
|
||||
): cv.All(cv.validate_bytes, cv.int_range(min=64, max=65535)),
|
||||
cv.Optional(CONF_FILTER, default="50us"): cv.All(
|
||||
cv.positive_time_period_microseconds,
|
||||
cv.Range(max=TimePeriod(microseconds=4294967295)),
|
||||
|
||||
@@ -14,7 +14,7 @@ static void IRAM_ATTR HOT write_value(RemoteReceiverComponentStore *arg, uint32_
|
||||
int32_t multiplier = ((int32_t) level << 1) - 1;
|
||||
uint32_t buffer_write = arg->buffer_write;
|
||||
arg->buffer[buffer_write++] = (int32_t) delta * multiplier;
|
||||
if (buffer_write >= arg->buffer_size) {
|
||||
if (buffer_write >= arg->buffer_entries) {
|
||||
buffer_write = 0;
|
||||
}
|
||||
|
||||
@@ -65,8 +65,9 @@ void RemoteReceiverComponent::setup() {
|
||||
this->store_.idle_us = this->idle_us_;
|
||||
this->store_.filter_us = this->filter_us_;
|
||||
this->store_.pin = this->pin_->to_isr();
|
||||
this->store_.buffer = new int32_t[this->buffer_size_];
|
||||
this->store_.buffer_size = this->buffer_size_;
|
||||
// rounded up so a size that is not a multiple of four never holds less than requested
|
||||
this->store_.buffer_entries = (this->buffer_size_ + sizeof(int32_t) - 1) / sizeof(int32_t);
|
||||
this->store_.buffer = new int32_t[this->store_.buffer_entries];
|
||||
this->store_.prev_micros = micros();
|
||||
this->store_.commit_micros = this->store_.prev_micros;
|
||||
this->store_.prev_level = this->pin_->digital_read();
|
||||
@@ -79,11 +80,11 @@ void RemoteReceiverComponent::dump_config() {
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
"Remote Receiver:\n"
|
||||
" Buffer Size: %" PRIu32 "\n"
|
||||
" Buffer Size: %" PRIu32 " bytes (%" PRIu32 " pulses)\n"
|
||||
" Tolerance: %" PRIu32 "%s\n"
|
||||
" Filter out pulses shorter than: %" PRIu32 " us\n"
|
||||
" Signal is done after %" PRIu32 " us of no changes",
|
||||
this->buffer_size_, this->tolerance_,
|
||||
this->buffer_size_, this->store_.buffer_entries, this->tolerance_,
|
||||
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"),
|
||||
this->filter_us_, this->idle_us_);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
@@ -119,7 +120,7 @@ void RemoteReceiverComponent::loop() {
|
||||
while (temp_read != last_index && (uint32_t) std::abs(s.buffer[temp_read]) < this->idle_us_) {
|
||||
reserve_size++;
|
||||
temp_read++;
|
||||
if (temp_read >= s.buffer_size) {
|
||||
if (temp_read >= s.buffer_entries) {
|
||||
temp_read = 0;
|
||||
}
|
||||
}
|
||||
@@ -129,7 +130,7 @@ void RemoteReceiverComponent::loop() {
|
||||
// read the buffer
|
||||
for (uint32_t i = 0; i < reserve_size + 1; i++) {
|
||||
this->temp_.push_back((int32_t) s.buffer[s.buffer_read++]);
|
||||
if (s.buffer_read >= s.buffer_size) {
|
||||
if (s.buffer_read >= s.buffer_entries) {
|
||||
s.buffer_read = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ struct RemoteReceiverComponentStore {
|
||||
uint32_t buffer_read{0};
|
||||
volatile uint32_t commit_micros{0};
|
||||
volatile uint32_t prev_micros{0};
|
||||
uint32_t buffer_size{1000};
|
||||
uint32_t buffer_entries{0};
|
||||
uint32_t filter_us{10};
|
||||
uint32_t idle_us{10000};
|
||||
ISRInternalGPIOPin pin;
|
||||
@@ -83,14 +83,14 @@ class RemoteReceiverComponent final : public remote_base::RemoteReceiverBase,
|
||||
protected:
|
||||
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||
void decode_rmt_(rmt_symbol_word_t *item, size_t item_count);
|
||||
// log the failed RMT call and mark the component failed
|
||||
void fail_(esp_err_t error, const LogString *reason);
|
||||
rmt_channel_handle_t channel_{NULL};
|
||||
uint32_t filter_symbols_{0};
|
||||
uint32_t receive_symbols_{0};
|
||||
bool with_dma_{false};
|
||||
uint32_t carrier_frequency_{0};
|
||||
uint8_t carrier_duty_percent_{100};
|
||||
esp_err_t error_code_{ESP_OK};
|
||||
std::string error_string_;
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_ESP32)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "remote_receiver.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/wake.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <soc/soc_caps.h>
|
||||
@@ -14,25 +15,37 @@ static constexpr uint32_t DEFAULT_BUFFER_SLOTS = 4;
|
||||
|
||||
static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) {
|
||||
RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg;
|
||||
rmt_rx_done_event_data_t *event_buffer = (rmt_rx_done_event_data_t *) (store->buffer + store->buffer_write);
|
||||
const uint32_t buffer_write = store->buffer_write;
|
||||
rmt_rx_done_event_data_t *event_buffer = (rmt_rx_done_event_data_t *) (store->buffer + buffer_write);
|
||||
uint32_t event_size = sizeof(rmt_rx_done_event_data_t);
|
||||
uint32_t next_write = store->buffer_write + event_size + event->num_symbols * sizeof(rmt_symbol_word_t);
|
||||
uint32_t next_write = buffer_write + event_size + event->num_symbols * sizeof(rmt_symbol_word_t);
|
||||
if (next_write + event_size + store->receive_size > store->buffer_size) {
|
||||
next_write = 0;
|
||||
}
|
||||
if (store->buffer_read - next_write < event_size + store->receive_size) {
|
||||
next_write = store->buffer_write;
|
||||
next_write = buffer_write;
|
||||
store->overflow = true;
|
||||
}
|
||||
if (event->num_symbols <= store->filter_symbols) {
|
||||
next_write = store->buffer_write;
|
||||
next_write = buffer_write;
|
||||
}
|
||||
store->error =
|
||||
rmt_receive(channel, (uint8_t *) store->buffer + next_write + event_size, store->receive_size, &store->config);
|
||||
event_buffer->num_symbols = event->num_symbols;
|
||||
event_buffer->received_symbols = event->received_symbols;
|
||||
const bool stored = next_write != buffer_write;
|
||||
store->buffer_write = next_write;
|
||||
return false;
|
||||
// a stored frame is decoded, and a failed re-arm reported, on the next loop pass instead of
|
||||
// waiting out the loop interval; filtered noise and dropped frames leave nothing to read
|
||||
BaseType_t task_woken = pdFALSE;
|
||||
if (stored || store->error != ESP_OK)
|
||||
wake_loop_isrsafe(&task_woken);
|
||||
return task_woken != pdFALSE;
|
||||
}
|
||||
|
||||
void RemoteReceiverComponent::fail_(esp_err_t error, const LogString *reason) {
|
||||
ESP_LOGE(TAG, "RMT driver failed: %s", esp_err_to_name(error));
|
||||
this->mark_failed(reason);
|
||||
}
|
||||
|
||||
void RemoteReceiverComponent::setup() {
|
||||
@@ -47,13 +60,8 @@ void RemoteReceiverComponent::setup() {
|
||||
channel.flags.with_dma = this->with_dma_;
|
||||
esp_err_t error = rmt_new_rx_channel(&channel, &this->channel_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
if (error == ESP_ERR_NOT_FOUND) {
|
||||
this->error_string_ = "out of RMT symbol memory";
|
||||
} else {
|
||||
this->error_string_ = "in rmt_new_rx_channel";
|
||||
}
|
||||
this->mark_failed();
|
||||
this->fail_(error,
|
||||
error == ESP_ERR_NOT_FOUND ? LOG_STR("out of RMT symbol memory") : LOG_STR("in rmt_new_rx_channel"));
|
||||
return;
|
||||
}
|
||||
if (this->pin_->get_flags() & gpio::FLAG_PULLUP) {
|
||||
@@ -63,9 +71,7 @@ void RemoteReceiverComponent::setup() {
|
||||
}
|
||||
error = rmt_enable(this->channel_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_enable";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_enable"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,9 +83,7 @@ void RemoteReceiverComponent::setup() {
|
||||
carrier.flags.polarity_active_low = this->pin_->is_inverted();
|
||||
error = rmt_apply_carrier(this->channel_, &carrier);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_apply_carrier";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_apply_carrier"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -89,9 +93,7 @@ void RemoteReceiverComponent::setup() {
|
||||
callbacks.on_recv_done = rmt_callback;
|
||||
error = rmt_rx_register_event_callbacks(this->channel_, &callbacks, &this->store_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_rx_register_event_callbacks";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_rx_register_event_callbacks"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,9 +116,7 @@ void RemoteReceiverComponent::setup() {
|
||||
error = rmt_receive(this->channel_, (uint8_t *) this->store_.buffer + event_size, this->store_.receive_size,
|
||||
&this->store_.config);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_receive";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_receive"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -140,18 +140,11 @@ void RemoteReceiverComponent::dump_config() {
|
||||
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"),
|
||||
this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_),
|
||||
this->error_string_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteReceiverComponent::loop() {
|
||||
if (this->store_.error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Receive error");
|
||||
this->error_code_ = this->store_.error;
|
||||
this->error_string_ = "in rmt_callback";
|
||||
this->mark_failed();
|
||||
this->fail_(this->store_.error, LOG_STR("in rmt_callback"));
|
||||
}
|
||||
if (this->store_.overflow) {
|
||||
ESP_LOGW(TAG, "Buffer overflow");
|
||||
|
||||
@@ -141,6 +141,8 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||
// log the failed RMT call and mark the component failed
|
||||
void fail_(esp_err_t error, const LogString *reason);
|
||||
void configure_rmt_();
|
||||
void wait_for_rmt_();
|
||||
|
||||
@@ -156,8 +158,6 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
|
||||
bool eot_level_{false};
|
||||
rmt_channel_handle_t channel_{NULL};
|
||||
rmt_encoder_handle_t encoder_{NULL};
|
||||
esp_err_t error_code_{ESP_OK};
|
||||
std::string error_string_;
|
||||
bool inverted_{false};
|
||||
bool non_blocking_{false};
|
||||
#endif
|
||||
|
||||
@@ -51,6 +51,11 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
|
||||
}
|
||||
#endif
|
||||
|
||||
void RemoteTransmitterComponent::fail_(esp_err_t error, const LogString *reason) {
|
||||
ESP_LOGE(TAG, "RMT driver failed: %s", esp_err_to_name(error));
|
||||
this->mark_failed(reason);
|
||||
}
|
||||
|
||||
void RemoteTransmitterComponent::setup() {
|
||||
this->inverted_ = this->pin_->is_inverted();
|
||||
this->configure_rmt_();
|
||||
@@ -67,11 +72,6 @@ void RemoteTransmitterComponent::dump_config() {
|
||||
if (this->current_carrier_frequency_ != 0 && this->carrier_duty_percent_ != 100) {
|
||||
ESP_LOGCONFIG(TAG, " Carrier Duty: %u%%", this->carrier_duty_percent_);
|
||||
}
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_),
|
||||
this->error_string_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteTransmitterComponent::digital_write(bool value) {
|
||||
@@ -129,13 +129,8 @@ void RemoteTransmitterComponent::configure_rmt_() {
|
||||
#endif
|
||||
error = rmt_new_tx_channel(&channel, &this->channel_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
if (error == ESP_ERR_NOT_FOUND) {
|
||||
this->error_string_ = "out of RMT symbol memory";
|
||||
} else {
|
||||
this->error_string_ = "in rmt_new_tx_channel";
|
||||
}
|
||||
this->mark_failed();
|
||||
this->fail_(error,
|
||||
error == ESP_ERR_NOT_FOUND ? LOG_STR("out of RMT symbol memory") : LOG_STR("in rmt_new_tx_channel"));
|
||||
return;
|
||||
}
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
@@ -159,9 +154,7 @@ void RemoteTransmitterComponent::configure_rmt_() {
|
||||
encoder.min_chunk_size = 1;
|
||||
error = rmt_new_simple_encoder(&encoder, &this->encoder_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_new_simple_encoder";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_new_simple_encoder"));
|
||||
return;
|
||||
}
|
||||
#else
|
||||
@@ -169,18 +162,14 @@ void RemoteTransmitterComponent::configure_rmt_() {
|
||||
memset(&encoder, 0, sizeof(encoder));
|
||||
error = rmt_new_copy_encoder(&encoder, &this->encoder_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_new_copy_encoder";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_new_copy_encoder"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
error = rmt_enable(this->channel_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_enable";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_enable"));
|
||||
return;
|
||||
}
|
||||
this->digital_write(open_drain || this->inverted_);
|
||||
@@ -199,9 +188,7 @@ void RemoteTransmitterComponent::configure_rmt_() {
|
||||
error = rmt_apply_carrier(this->channel_, &carrier);
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_apply_carrier";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_apply_carrier"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,20 +197,20 @@ def _parse_platform_version(value: Any) -> str:
|
||||
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/earlephilhower/arduino-pico/releases
|
||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(6, 0, 0)
|
||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(6, 1, 0)
|
||||
|
||||
# The raspberrypi platform version to use for arduino frameworks
|
||||
# - https://github.com/maxgerhardt/platform-raspberrypi/tags
|
||||
# develop-branch commit carrying the arduino-pico 6.0.0 / pico-quick-toolchain
|
||||
# 5.0.0 (GCC 16.1) update; replace with a release tag when one is cut
|
||||
RECOMMENDED_ARDUINO_PLATFORM_VERSION = "9c167c6b8aac4f4cfa6d55a0c4e5b848795150c0"
|
||||
# develop-branch commit carrying the arduino-pico 6.1.0 update and the board
|
||||
# JSON files it adds; replace with a release tag when one is cut
|
||||
RECOMMENDED_ARDUINO_PLATFORM_VERSION = "5d4561a05e3b212660ac6fdd3fbfb328d1988aa1"
|
||||
|
||||
|
||||
def _arduino_check_versions(value: ConfigType) -> ConfigType:
|
||||
value = value.copy()
|
||||
lookups = {
|
||||
"dev": (cv.Version(6, 0, 0), "https://github.com/earlephilhower/arduino-pico"),
|
||||
"latest": (cv.Version(6, 0, 0), None),
|
||||
"dev": (cv.Version(6, 1, 0), "https://github.com/earlephilhower/arduino-pico"),
|
||||
"latest": (cv.Version(6, 1, 0), None),
|
||||
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
|
||||
}
|
||||
|
||||
|
||||
@@ -1135,6 +1135,18 @@ RP2_BOARD_PINS = {
|
||||
"SS": 5,
|
||||
"TX": 0,
|
||||
},
|
||||
"soldered_nula_node_rp2040": {
|
||||
"MISO": 16,
|
||||
"MOSI": 19,
|
||||
"RX": 1,
|
||||
"SCK": 18,
|
||||
"SCL": 9,
|
||||
"SCL1": 11,
|
||||
"SDA": 8,
|
||||
"SDA1": 10,
|
||||
"SS": 17,
|
||||
"TX": 0,
|
||||
},
|
||||
"soldered_nula_rp2350": {
|
||||
"MISO": 2,
|
||||
"MOSI": 3,
|
||||
@@ -2127,6 +2139,12 @@ BOARDS = {
|
||||
"mcu": "rp2040",
|
||||
"max_pin": 29,
|
||||
},
|
||||
"soldered_nula_node_rp2040": {
|
||||
"name": "Soldered Electronics NULA Node",
|
||||
"mcu": "rp2040",
|
||||
"max_pin": 29,
|
||||
"wifi": True,
|
||||
},
|
||||
"soldered_nula_rp2350": {
|
||||
"name": "Soldered Electronics NULA RP2350",
|
||||
"mcu": "rp2350",
|
||||
|
||||
@@ -55,8 +55,7 @@ namespace esphome::rp2 {
|
||||
|
||||
static const char *const TAG = "rp2.crash";
|
||||
|
||||
// Placed in .noinit so BSS zero-init cannot race with crash_handler_read_and_clear().
|
||||
// The valid field is explicitly cleared in crash_handler_read_and_clear() instead.
|
||||
// Filled from the watchdog scratch registers on the first read.
|
||||
static struct CrashData {
|
||||
bool valid;
|
||||
uint32_t pc;
|
||||
@@ -64,11 +63,24 @@ static struct CrashData {
|
||||
uint32_t sp;
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint8_t backtrace_count;
|
||||
} s_crash_data __attribute__((section(".noinit"))); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
} s_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool crash_handler_has_data() { return s_crash_data.valid; }
|
||||
// Logger::pre_setup() logs the record before App.pre_setup() reaches
|
||||
// arch_init(), so the first caller reads it and later calls are no-ops.
|
||||
// The read clears the scratch registers, so it must not run twice, and
|
||||
// arch_init() keeps its call so the read precedes watchdog_enable(), which
|
||||
// overwrites scratch[4].
|
||||
static bool s_crash_data_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool crash_handler_has_data() {
|
||||
crash_handler_read_and_clear();
|
||||
return s_crash_data.valid;
|
||||
}
|
||||
|
||||
void crash_handler_read_and_clear() {
|
||||
if (s_crash_data_read)
|
||||
return;
|
||||
s_crash_data_read = true;
|
||||
s_crash_data.valid = false;
|
||||
uint32_t magic = watchdog_hw->scratch[0];
|
||||
if ((magic & 0xFFFF0000) == CRASH_MAGIC_SENTINEL && (magic & 0xFFFF) == CRASH_DATA_VERSION) {
|
||||
@@ -97,7 +109,7 @@ void crash_handler_read_and_clear() {
|
||||
// the device crashes again during boot, and allowing the CLI's process_stacktrace
|
||||
// to match and decode each address individually.
|
||||
void crash_handler_log() {
|
||||
if (!s_crash_data.valid)
|
||||
if (!crash_handler_has_data())
|
||||
return;
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
namespace esphome::rp2 {
|
||||
|
||||
/// Read crash data from watchdog scratch registers and clear them.
|
||||
/// Only the first call reads; later calls are no-ops.
|
||||
void crash_handler_read_and_clear();
|
||||
|
||||
/// Log crash data if a crash was detected on previous boot.
|
||||
void crash_handler_log();
|
||||
|
||||
/// Returns true if crash data was found this boot.
|
||||
/// Returns true if crash data was found this boot, reading it first if needed.
|
||||
bool crash_handler_has_data();
|
||||
|
||||
} // namespace esphome::rp2
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
namespace esphome::rp2040_ble {
|
||||
namespace {
|
||||
|
||||
// Pinned against arduino-pico 6.0.0's prebuilt archives: a framework bump (or
|
||||
// Pinned against arduino-pico 6.1.0's prebuilt archives: a framework bump (or
|
||||
// a changed ENABLE_* macro) shifting the struct layout must fail the build
|
||||
// here, not overrun the pool blocks at runtime. Sizes differ per core
|
||||
// architecture (measured from each archive's own storage symbols). GCC only:
|
||||
|
||||
@@ -101,7 +101,7 @@ int RpiDpiRgb::get_height() {
|
||||
}
|
||||
|
||||
void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
if (this->is_point_clipped(x, y))
|
||||
return; // NOLINT
|
||||
|
||||
switch (this->rotation_) {
|
||||
@@ -124,7 +124,7 @@ void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) {
|
||||
|
||||
this->draw_pixels_at(x, y, 1, 1, (const uint8_t *) &pixel, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, true,
|
||||
0, 0, 0);
|
||||
App.feed_wdt();
|
||||
this->feed_wdt_per_pixel_();
|
||||
}
|
||||
|
||||
void RpiDpiRgb::dump_config() {
|
||||
|
||||
@@ -164,7 +164,7 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
|
||||
}
|
||||
|
||||
void Sdl::draw_pixel_at(int x, int y, Color color) {
|
||||
if (this->texture_ == nullptr || !this->get_clipping().inside(x, y))
|
||||
if (this->texture_ == nullptr || this->is_point_clipped(x, y))
|
||||
return;
|
||||
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||
|
||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass, field
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, network, psram, socket, wifi
|
||||
from esphome.components import esp32, mdns, network, psram, socket, wifi
|
||||
from esphome.components.const import CONF_MANUFACTURER
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
CONF_FORMAT,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_MDNS,
|
||||
CONF_MODEL,
|
||||
CONF_NAME,
|
||||
CONF_PROJECT,
|
||||
@@ -281,10 +282,15 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(setter(value))
|
||||
|
||||
# sendspin-cpp library
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.2")
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.8.0")
|
||||
|
||||
cg.add_define("USE_SENDSPIN", True) # for MDNS
|
||||
|
||||
# Service starts disabled and the hub enables it; always advertised where unsupported
|
||||
if mdns.request_service_enable_disable():
|
||||
mdns_var = await cg.get_variable(CORE.config[CONF_MDNS][CONF_ID])
|
||||
cg.add(var.set_mdns(mdns_var))
|
||||
|
||||
data = _get_data()
|
||||
|
||||
# The color role is not yet wired up in ESPHome; disable it in the library for now.
|
||||
|
||||
@@ -97,6 +97,10 @@ void SendspinMediaPlayer::control(const media_player::MediaPlayerCall &call) {
|
||||
// Ignore any commands sent before the media player is setup
|
||||
return;
|
||||
}
|
||||
if (!this->parent_->is_client_running()) {
|
||||
ESP_LOGW(TAG, "Cannot control media player: Sendspin is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
auto volume = call.get_volume();
|
||||
if (volume.has_value()) {
|
||||
|
||||
@@ -45,6 +45,8 @@ bool SendspinMediaSource::can_handle(const std::string &uri) const { return uri.
|
||||
|
||||
// THREAD CONTEXT: Main loop (media_source.h documents play_uri as main-loop only)
|
||||
bool SendspinMediaSource::play_uri(const std::string &uri) {
|
||||
// The queued request has been delivered, whatever the outcome, so the next stream start may request again
|
||||
this->pending_start_ = false;
|
||||
if (!this->is_ready() || this->is_failed() || !this->has_listener()) {
|
||||
return false;
|
||||
}
|
||||
@@ -54,6 +56,11 @@ bool SendspinMediaSource::play_uri(const std::string &uri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->parent_->is_client_running()) {
|
||||
ESP_LOGE(TAG, "Cannot play '%s': Sendspin is disabled", uri.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uri.starts_with(URI_PREFIX)) {
|
||||
ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
|
||||
return false;
|
||||
@@ -74,7 +81,6 @@ bool SendspinMediaSource::play_uri(const std::string &uri) {
|
||||
}
|
||||
|
||||
// Tell the orchestrator we're now playing so it routes audio output from us
|
||||
this->pending_start_ = false;
|
||||
this->set_state_(media_source::MediaSourceState::PLAYING);
|
||||
|
||||
return true;
|
||||
@@ -82,6 +88,15 @@ bool SendspinMediaSource::play_uri(const std::string &uri) {
|
||||
|
||||
// THREAD CONTEXT: Main loop (media_source.h documents handle_command as main-loop only)
|
||||
void SendspinMediaSource::handle_command(media_source::MediaSourceCommand command) {
|
||||
if (!this->parent_->is_client_running()) {
|
||||
if (command == media_source::MediaSourceCommand::STOP) {
|
||||
// Nothing is playing, so the orchestrator gets its pipeline back straight away
|
||||
this->on_stream_end();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Cannot handle command: Sendspin is disabled");
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (command) {
|
||||
case media_source::MediaSourceCommand::STOP: {
|
||||
if (!this->pending_start_) {
|
||||
|
||||
@@ -62,14 +62,26 @@ void SendspinHub::setup() {
|
||||
this->client_->add_player(this->player_config_).set_listener(this->player_listener_);
|
||||
#endif
|
||||
|
||||
if (!this->client_->start_server()) {
|
||||
ESP_LOGE(TAG, "Failed to start Sendspin server");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#ifndef USE_SENDSPIN_SWITCH
|
||||
this->enabled_ = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SendspinHub::loop() { this->client_->loop(); }
|
||||
void SendspinHub::loop() {
|
||||
if (this->enabled_.has_value() && this->enabled_.value() != this->client_->is_started() &&
|
||||
!this->status_has_error()) {
|
||||
if (!this->enabled_.value()) {
|
||||
this->client_->stop();
|
||||
} else if (!this->client_->start()) {
|
||||
this->status_set_error(LOG_STR("Failed to start Sendspin client"));
|
||||
}
|
||||
}
|
||||
this->client_->loop();
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
this->update_mdns_service_();
|
||||
#endif
|
||||
}
|
||||
|
||||
void SendspinHub::dump_config() {
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
@@ -96,25 +108,54 @@ void SendspinHub::dump_config() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
void SendspinHub::set_enabled(bool enabled) {
|
||||
if (this->status_has_error()) {
|
||||
ESP_LOGE(TAG, "Cannot %s: Sendspin failed to start, reboot to retry",
|
||||
enabled ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"));
|
||||
return;
|
||||
}
|
||||
this->enabled_ = enabled;
|
||||
}
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
// THREAD CONTEXT: Main loop
|
||||
void SendspinHub::update_mdns_service_() {
|
||||
// Synced from loop() because mdns sets up after this hub and only builds its service list then.
|
||||
if (!this->mdns_->is_ready()) {
|
||||
return;
|
||||
}
|
||||
bool advertise = this->client_->is_started();
|
||||
if (advertise == this->mdns_advertised_) {
|
||||
return;
|
||||
}
|
||||
// One attempt per change
|
||||
this->mdns_advertised_ = advertise;
|
||||
if (!this->mdns_->set_service_enabled("_sendspin", "_tcp", advertise)) {
|
||||
ESP_LOGE(TAG, "Failed to %s mDNS service", advertise ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Delegating methods ---
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
void SendspinHub::connect_to_server(const std::string &url) {
|
||||
if (this->is_ready()) {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->connect_to(url);
|
||||
}
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
void SendspinHub::disconnect_from_server(sendspin::SendspinGoodbyeReason reason) {
|
||||
if (this->is_ready()) {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->disconnect(reason);
|
||||
}
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
void SendspinHub::update_state(sendspin::SendspinClientState state) {
|
||||
if (this->is_ready()) {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->update_state(state);
|
||||
}
|
||||
}
|
||||
@@ -233,7 +274,7 @@ void SendspinHub::artwork_frame_done(uint8_t slot) {
|
||||
// THREAD CONTEXT: Main loop (invoked from ESPHome actions / other components)
|
||||
void SendspinHub::send_client_command(sendspin::SendspinControllerCommand command, std::optional<uint8_t> volume,
|
||||
std::optional<bool> mute) {
|
||||
if (this->is_ready()) {
|
||||
if (this->is_client_running()) {
|
||||
sendspin::ClientCommandControllerObject obj = {
|
||||
.command = command,
|
||||
.volume = volume,
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/version.h"
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
#include "esphome/components/mdns/mdns_component.h"
|
||||
#endif
|
||||
|
||||
#include <sendspin/client.h>
|
||||
#include <sendspin/config.h>
|
||||
#include <sendspin/types.h>
|
||||
@@ -93,7 +97,7 @@ class SendspinHub final : public Component,
|
||||
|
||||
/// @brief Connects the underlying client to the given Sendspin server.
|
||||
///
|
||||
/// No-op if the hub's client is not ready (e.g. setup() has not completed).
|
||||
/// No-op if the hub's client is not running (see is_client_running()).
|
||||
/// Must be called from the main loop thread.
|
||||
/// @param url WebSocket URL of the Sendspin server, starting with `ws://` (e.g. `ws://host:port/path`).
|
||||
void connect_to_server(const std::string &url);
|
||||
@@ -101,7 +105,7 @@ class SendspinHub final : public Component,
|
||||
/// @brief Disconnects the underlying client from the current server.
|
||||
///
|
||||
/// Sends a `client/goodbye` message with the given reason before closing the connection.
|
||||
/// No-op if the hub's client is not ready. Must be called from the main loop thread.
|
||||
/// No-op if the hub's client is not running. Must be called from the main loop thread.
|
||||
/// @param reason Reason reported to the server:
|
||||
/// - `ANOTHER_SERVER`: client is switching to another server.
|
||||
/// - `SHUTDOWN`: client is shutting down.
|
||||
@@ -111,7 +115,7 @@ class SendspinHub final : public Component,
|
||||
|
||||
/// @brief Updates the client's reported playback state on the server.
|
||||
///
|
||||
/// No-op if the hub's client is not ready. Must be called from the main loop thread.
|
||||
/// No-op if the hub's client is not running. Must be called from the main loop thread.
|
||||
/// @param state New client state:
|
||||
/// - `SYNCHRONIZED`: client is synchronized and playing from the server.
|
||||
/// - `ERROR`: client encountered a playback error.
|
||||
@@ -126,6 +130,17 @@ class SendspinHub final : public Component,
|
||||
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
|
||||
|
||||
/// @brief Requests the Sendspin client, including the server, the roles and the mDNS advertisement, to start or
|
||||
/// stop.
|
||||
///
|
||||
/// Applied from the hub's loop(). Stopping blocks until the client is fully stopped; the roles' clear callbacks
|
||||
/// fire from inside that call. With a sendspin switch configured the client stays stopped until the switch has
|
||||
/// called this once. Must be called from the main loop thread.
|
||||
void set_enabled(bool enabled);
|
||||
|
||||
/// @brief Returns whether the Sendspin client is running.
|
||||
bool is_client_running() const { return this->client_ != nullptr && this->client_->is_started(); }
|
||||
|
||||
/// @brief Sets the device information reported to the server in the `client/hello` message.
|
||||
///
|
||||
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
|
||||
@@ -135,6 +150,10 @@ class SendspinHub final : public Component,
|
||||
void set_model(const char *model) { this->model_ = model; }
|
||||
void set_firmware_version(const char *firmware_version) { this->firmware_version_ = firmware_version; }
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
void set_mdns(mdns::MDNSComponent *mdns) { this->mdns_ = mdns; }
|
||||
#endif
|
||||
|
||||
// --- Sendspin role specific methods ---
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
@@ -204,6 +223,11 @@ class SendspinHub final : public Component,
|
||||
/// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi).
|
||||
static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
/// @brief Keeps the `_sendspin` mDNS service advertised while the client is running.
|
||||
void update_mdns_service_();
|
||||
#endif
|
||||
|
||||
// --- SendspinClientListener overrides ---
|
||||
void on_group_update(const sendspin::GroupUpdateObject &group) override;
|
||||
|
||||
@@ -282,11 +306,19 @@ class SendspinHub final : public Component,
|
||||
|
||||
bool task_stack_in_psram_{false};
|
||||
|
||||
// Requested client state, applied from loop(). Empty until the switch restores its state.
|
||||
std::optional<bool> enabled_;
|
||||
|
||||
// Device information sent in the `client/hello` message. Defaults apply when neither the
|
||||
// sendspin configuration nor the project information supplies a value.
|
||||
const char *manufacturer_{"ESPHome"};
|
||||
const char *model_{nullptr}; // nullptr reports the device name instead
|
||||
const char *firmware_version_{ESPHOME_VERSION};
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
mdns::MDNSComponent *mdns_{nullptr};
|
||||
bool mdns_advertised_{false}; // Last state requested from mdns
|
||||
#endif
|
||||
};
|
||||
|
||||
/// @brief Base class for all sendspin subcomponents.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_SENDSPIN_ID, SendspinHub, sendspin_ns
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
DEPENDENCIES = ["sendspin"]
|
||||
|
||||
SendspinSwitch = sendspin_ns.class_("SendspinSwitch", switch.Switch, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
switch.switch_schema(
|
||||
SendspinSwitch,
|
||||
block_inverted=True,
|
||||
default_restore_mode="RESTORE_DEFAULT_ON",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
.extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on_esp32,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_SENDSPIN_ID])
|
||||
cg.add_define("USE_SENDSPIN_SWITCH", True)
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "sendspin_switch.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
static const char *const TAG = "sendspin.switch";
|
||||
|
||||
void SendspinSwitch::setup() {
|
||||
// The hub waits for this request, so a restore mode without a state still has to answer.
|
||||
this->control(this->get_initial_state_with_restore_mode().value_or(true));
|
||||
}
|
||||
|
||||
void SendspinSwitch::dump_config() { LOG_SWITCH("", "Sendspin Switch", this); }
|
||||
|
||||
// THREAD CONTEXT: Main loop
|
||||
void SendspinSwitch::write_state(bool state) {
|
||||
this->parent_->set_enabled(state);
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
/// @brief Switch that starts and stops the Sendspin client through the hub (see SendspinHub::set_enabled()).
|
||||
class SendspinSwitch final : public switch_::Switch, public SendspinChild {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -40,10 +40,7 @@ const LogString *state_class_to_string(StateClass state_class) {
|
||||
return StateClassStrings::get_log_str(static_cast<uint8_t>(state_class), 0);
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
Sensor::Sensor() : state(NAN), raw_state(NAN) {}
|
||||
#pragma GCC diagnostic pop
|
||||
Sensor::Sensor() : state(NAN) {}
|
||||
|
||||
int8_t Sensor::get_accuracy_decimals() {
|
||||
if (this->sensor_flags_.has_accuracy_override)
|
||||
@@ -66,11 +63,8 @@ StateClass Sensor::get_state_class() {
|
||||
}
|
||||
|
||||
void Sensor::publish_state(float state) {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
this->raw_state = state;
|
||||
#pragma GCC diagnostic pop
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
this->raw_state_ = state;
|
||||
this->raw_callback_.call(state);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -96,18 +96,20 @@ class Sensor : public EntityBase {
|
||||
|
||||
/// Getter-syntax for .state.
|
||||
float get_state() const { return this->state; }
|
||||
/// Getter-syntax for .raw_state
|
||||
/// Get the last state received by publish_state(), before any filters were applied.
|
||||
float get_raw_state() const {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return this->raw_state;
|
||||
#pragma GCC diagnostic pop
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
return this->raw_state_;
|
||||
#else
|
||||
return this->state; // No filters compiled in, raw == filtered
|
||||
#endif
|
||||
}
|
||||
|
||||
/** Publish a new state to the front-end.
|
||||
*
|
||||
* First, the new state will be assigned to the raw_value. Then it's passed through all filters
|
||||
* until it finally lands in the .value member variable and a callback is issued.
|
||||
* The value is passed through the filter chain (when filters are compiled in) before landing in
|
||||
* the `state` member and triggering the state callback. The pre-filter value is available via
|
||||
* get_raw_state().
|
||||
*
|
||||
* @param state The state as a floating point number.
|
||||
*/
|
||||
@@ -137,17 +139,11 @@ class Sensor : public EntityBase {
|
||||
*/
|
||||
float state;
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
/// @deprecated Use get_raw_state() instead. This member will be removed in ESPHome 2026.10.0.
|
||||
ESPDEPRECATED("Use get_raw_state() instead of .raw_state. Will be removed in 2026.10.0", "2026.4.0")
|
||||
float raw_state;
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
void internal_send_state_to_frontend(float state);
|
||||
|
||||
protected:
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
float raw_state_{NAN}; ///< The last state passed to publish_state(), before filters.
|
||||
LazyCallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
|
||||
#endif
|
||||
LazyCallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Speaker Media Player Setup."""
|
||||
|
||||
import logging
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import (
|
||||
@@ -33,9 +31,6 @@ from esphome.const import (
|
||||
CONF_TASK_STACK_IN_PSRAM,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
AUTO_LOAD = ["audio"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
@@ -44,7 +39,7 @@ DOMAIN = "media_player"
|
||||
|
||||
CONF_ANNOUNCEMENT = "announcement"
|
||||
CONF_ANNOUNCEMENT_PIPELINE = "announcement_pipeline"
|
||||
CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" # Remove before 2026.10.0
|
||||
CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" # Remove before 2027.4.0
|
||||
CONF_ENQUEUE = "enqueue"
|
||||
CONF_MEDIA_FILE = "media_file"
|
||||
CONF_MEDIA_PIPELINE = "media_pipeline"
|
||||
@@ -103,15 +98,6 @@ def _validate_repeated_speaker(config):
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
# Remove before 2026.10.0
|
||||
if CONF_CODEC_SUPPORT_ENABLED in config:
|
||||
_LOGGER.warning(
|
||||
"'%s' is deprecated and will be removed in 2026.10.0. "
|
||||
"Codec support is now automatically determined from the pipeline "
|
||||
"'format' setting. Set format to 'NONE' to enable all codecs.",
|
||||
CONF_CODEC_SUPPORT_ENABLED,
|
||||
)
|
||||
|
||||
# Request codecs based on pipeline formats. Codecs needed by local files are
|
||||
# already requested during CONFIG_SCHEMA validation (via audio_files_schema).
|
||||
media_player.request_codecs_for_format_configs(
|
||||
@@ -151,8 +137,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
|
||||
min=4000, max=4000000
|
||||
),
|
||||
# Remove before 2026.10.0
|
||||
cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.Any(cv.boolean, cv.string),
|
||||
# Removed in 2026.10.0 - kept to provide helpful error message
|
||||
cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.invalid(
|
||||
"The 'codec_support_enabled' option has been removed in ESPHome 2026.10.0.\n"
|
||||
"Codec support is now determined from the pipeline 'format' setting.\n"
|
||||
"Set 'format: NONE' on the pipeline to enable all codecs."
|
||||
),
|
||||
cv.Optional(CONF_FILES): audio_file.audio_files_schema(),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
|
||||
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,
|
||||
|
||||
@@ -546,11 +546,7 @@ void Sprinkler::set_auto_advance(const bool auto_advance) {
|
||||
if (this->auto_adv_sw_->state == auto_advance) {
|
||||
return;
|
||||
}
|
||||
if (auto_advance) {
|
||||
this->auto_adv_sw_->turn_on();
|
||||
} else {
|
||||
this->auto_adv_sw_->turn_off();
|
||||
}
|
||||
this->auto_adv_sw_->control(auto_advance);
|
||||
}
|
||||
|
||||
void Sprinkler::set_repeat(optional<uint32_t> repeat) {
|
||||
@@ -573,11 +569,7 @@ void Sprinkler::set_queue_enable(bool queue_enable) {
|
||||
if (this->queue_enable_sw_->state == queue_enable) {
|
||||
return;
|
||||
}
|
||||
if (queue_enable) {
|
||||
this->queue_enable_sw_->turn_on();
|
||||
} else {
|
||||
this->queue_enable_sw_->turn_off();
|
||||
}
|
||||
this->queue_enable_sw_->control(queue_enable);
|
||||
}
|
||||
|
||||
void Sprinkler::set_reverse(const bool reverse) {
|
||||
@@ -587,11 +579,7 @@ void Sprinkler::set_reverse(const bool reverse) {
|
||||
if (this->reverse_sw_->state == reverse) {
|
||||
return;
|
||||
}
|
||||
if (reverse) {
|
||||
this->reverse_sw_->turn_on();
|
||||
} else {
|
||||
this->reverse_sw_->turn_off();
|
||||
}
|
||||
this->reverse_sw_->control(reverse);
|
||||
}
|
||||
|
||||
void Sprinkler::set_standby(const bool standby) {
|
||||
@@ -601,11 +589,7 @@ void Sprinkler::set_standby(const bool standby) {
|
||||
if (this->standby_sw_->state == standby) {
|
||||
return;
|
||||
}
|
||||
if (standby) {
|
||||
this->standby_sw_->turn_on();
|
||||
} else {
|
||||
this->standby_sw_->turn_off();
|
||||
}
|
||||
this->standby_sw_->control(standby);
|
||||
}
|
||||
|
||||
uint32_t Sprinkler::valve_run_duration(const size_t valve_number) {
|
||||
|
||||
@@ -84,7 +84,7 @@ void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
|
||||
}
|
||||
|
||||
void ST7701S::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
if (this->is_point_clipped(x, y))
|
||||
return; // NOLINT
|
||||
|
||||
switch (this->rotation_) {
|
||||
@@ -107,7 +107,7 @@ void ST7701S::draw_pixel_at(int x, int y, Color color) {
|
||||
|
||||
this->draw_pixels_at(x, y, 1, 1, (const uint8_t *) &pixel, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, true,
|
||||
0, 0, 0);
|
||||
App.feed_wdt();
|
||||
this->feed_wdt_per_pixel_();
|
||||
}
|
||||
|
||||
void ST7701S::write_command_(uint8_t value) {
|
||||
|
||||
@@ -10,7 +10,6 @@ static const char *const TAG = "switch";
|
||||
Switch::Switch() : state(false) {}
|
||||
|
||||
void Switch::control(bool target_state) {
|
||||
ESP_LOGV(TAG, "'%s' Control: %s", this->get_name().c_str(), ONOFF(target_state));
|
||||
if (target_state) {
|
||||
this->turn_on();
|
||||
} else {
|
||||
|
||||
@@ -65,9 +65,6 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; }
|
||||
bool get_all_sensors_ready() { return this->sensors_ready_; };
|
||||
void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
// Remove before 2026.10.0
|
||||
ESPDEPRECATED("bypass_before_arming() is deprecated and will be removed in 2026.10.0", "2026.4.0")
|
||||
void bypass_before_arming() { this->auto_bypass_sensors_(); }
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
/** Initialize the sensors vector with the specified capacity.
|
||||
|
||||
@@ -42,11 +42,7 @@ void TemplateSwitch::setup() {
|
||||
if (initial_state.has_value()) {
|
||||
ESP_LOGD(TAG, " Restored state %s", ONOFF(initial_state.value()));
|
||||
// if it has a value, restore_mode is not "DISABLED", therefore act on the switch:
|
||||
if (initial_state.value()) {
|
||||
this->turn_on();
|
||||
} else {
|
||||
this->turn_off();
|
||||
}
|
||||
this->control(initial_state.value());
|
||||
}
|
||||
}
|
||||
void TemplateSwitch::dump_config() {
|
||||
|
||||
@@ -66,9 +66,12 @@ static const char *const TAG = "web_server";
|
||||
// GET /{domain}/{device_name}/{entity_name} - sub-device state (USE_DEVICES only)
|
||||
// POST /{domain}/{device_name}/{entity_name}/{action} - sub-device action (USE_DEVICES only)
|
||||
static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, bool is_post = false) {
|
||||
// Every path returns this one object so it is built in place; fields are only set once the URL is known valid
|
||||
UrlMatch match{};
|
||||
|
||||
// URL must start with '/' and have content after it
|
||||
if (url_len < 2 || url_ptr[0] != '/')
|
||||
return UrlMatch{};
|
||||
return match;
|
||||
|
||||
const char *p = url_ptr + 1;
|
||||
const char *end = url_ptr + url_len;
|
||||
@@ -90,15 +93,14 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain,
|
||||
|
||||
// Must have domain with trailing slash
|
||||
if (!s2)
|
||||
return UrlMatch{};
|
||||
|
||||
UrlMatch match{};
|
||||
match.domain = make_ref(s1, s2);
|
||||
match.valid = true;
|
||||
|
||||
if (only_domain || s2 >= end)
|
||||
return match;
|
||||
|
||||
if (only_domain || s2 >= end) {
|
||||
match.domain = make_ref(s1, s2);
|
||||
match.valid = true;
|
||||
return match;
|
||||
}
|
||||
|
||||
// Parse remaining segments only when needed
|
||||
const char *s3 = next_segment(s2);
|
||||
const char *s4 = s3 ? next_segment(s3) : nullptr;
|
||||
@@ -109,7 +111,7 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain,
|
||||
|
||||
// Reject empty segments
|
||||
if (seg2.empty() || (s3 && seg3.empty()) || (s4 && seg4.empty()))
|
||||
return UrlMatch{};
|
||||
return match;
|
||||
|
||||
// Interpret based on segment count
|
||||
if (!s3) {
|
||||
@@ -121,28 +123,31 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain,
|
||||
if (is_post) {
|
||||
match.id = seg2;
|
||||
match.method = seg3;
|
||||
return match;
|
||||
}
|
||||
} else {
|
||||
#ifdef USE_DEVICES
|
||||
match.device_name = seg2;
|
||||
match.id = seg3;
|
||||
match.device_name = seg2;
|
||||
match.id = seg3;
|
||||
#else
|
||||
return UrlMatch{}; // 3-segment GET not supported without USE_DEVICES
|
||||
return match; // 3-segment GET not supported without USE_DEVICES
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
// 3 segments after domain: /{domain}/{device}/{entity}/{action}
|
||||
#ifdef USE_DEVICES
|
||||
if (!is_post) {
|
||||
return UrlMatch{}; // 4-segment GET not supported (action requires POST)
|
||||
return match; // 4-segment GET not supported (action requires POST)
|
||||
}
|
||||
match.device_name = seg2;
|
||||
match.id = seg3;
|
||||
match.method = seg4;
|
||||
#else
|
||||
return UrlMatch{}; // Not supported without USE_DEVICES
|
||||
// Not supported without USE_DEVICES
|
||||
return match;
|
||||
#endif
|
||||
}
|
||||
|
||||
match.domain = make_ref(s1, s2);
|
||||
match.valid = true;
|
||||
return match;
|
||||
}
|
||||
|
||||
@@ -336,6 +341,9 @@ void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSou
|
||||
|
||||
WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {}
|
||||
|
||||
// Kept out of the callers so the 64 bit division is emitted once
|
||||
__attribute__((noinline)) static uint32_t uptime_seconds() { return static_cast<uint32_t>(millis_64() / 1000); }
|
||||
|
||||
json::SerializationBuffer<> WebServer::get_config_json() {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
@@ -343,7 +351,7 @@ json::SerializationBuffer<> WebServer::get_config_json() {
|
||||
root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str();
|
||||
char comment_buffer[Application::ESPHOME_COMMENT_SIZE_MAX];
|
||||
App.get_comment_string(comment_buffer);
|
||||
root[ESPHOME_F("comment")] = comment_buffer;
|
||||
root[ESPHOME_F("comment")] = static_cast<const char *>(comment_buffer);
|
||||
#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
|
||||
root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
|
||||
#else
|
||||
@@ -351,7 +359,7 @@ json::SerializationBuffer<> WebServer::get_config_json() {
|
||||
#endif
|
||||
root[ESPHOME_F("log")] = this->expose_log_;
|
||||
root[ESPHOME_F("lang")] = "en";
|
||||
root[ESPHOME_F("uptime")] = static_cast<uint32_t>(millis_64() / 1000);
|
||||
root[ESPHOME_F("uptime")] = uptime_seconds();
|
||||
|
||||
return builder.serialize();
|
||||
}
|
||||
@@ -382,7 +390,7 @@ void WebServer::setup() {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
char buf[32];
|
||||
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
|
||||
auto uptime = uptime_seconds();
|
||||
size_t len = buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
|
||||
this->events_.try_send_nodefer(buf, len, "ping", millis(), 30000);
|
||||
});
|
||||
@@ -467,7 +475,10 @@ bool WebServer::is_request_origin_allowed_(AsyncWebServerRequest *request, const
|
||||
const size_t scheme_sep = origin.find("://");
|
||||
if (scheme_sep != std::string::npos) {
|
||||
const std::string host = get_request_header(request, "Host");
|
||||
if (!host.empty() && origin.compare(scheme_sep + 3, std::string::npos, host) == 0)
|
||||
// Compare by hand: compare(pos, ...) carries an out_of_range throw path that can never fire here
|
||||
const size_t authority = scheme_sep + 3;
|
||||
if (!host.empty() && origin.size() - authority == host.size() &&
|
||||
memcmp(origin.data() + authority, host.data(), host.size()) == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -534,7 +545,7 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
|
||||
// Helper functions to reduce code size by avoiding macro expansion
|
||||
// Build unique id as: {domain}/{device_name}/{entity_name} or {domain}/{entity_name}
|
||||
// Uses names (not object_id) to avoid UTF-8 collision issues
|
||||
static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
|
||||
static void set_json_id(JsonObject root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
|
||||
const StringRef &name = obj->get_name();
|
||||
size_t prefix_len = strlen(prefix);
|
||||
size_t name_len = name.size();
|
||||
@@ -569,7 +580,7 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
|
||||
#endif
|
||||
memcpy(p, name.c_str(), name_len);
|
||||
p[name_len] = '\0';
|
||||
root[ESPHOME_F("id")] = id_buf;
|
||||
root[ESPHOME_F("id")] = static_cast<const char *>(id_buf);
|
||||
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root[ESPHOME_F("domain")] = prefix;
|
||||
@@ -594,14 +605,13 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
|
||||
// Keep as separate function even though only used once: reduces code size by ~48 bytes
|
||||
// by allowing compiler to share code between template instantiations (bool, float, etc.)
|
||||
template<typename T>
|
||||
static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value,
|
||||
JsonDetail start_config) {
|
||||
static void set_json_value(JsonObject root, EntityBase *obj, const char *prefix, T value, JsonDetail start_config) {
|
||||
set_json_id(root, obj, prefix, start_config);
|
||||
root[ESPHOME_F("value")] = value;
|
||||
}
|
||||
|
||||
template<typename S, typename T>
|
||||
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, S state, const T &value,
|
||||
static void set_json_icon_state_value(JsonObject root, EntityBase *obj, const char *prefix, S state, T value,
|
||||
JsonDetail start_config) {
|
||||
set_json_value(root, obj, prefix, value, start_config);
|
||||
root[ESPHOME_F("state")] = state;
|
||||
@@ -1230,7 +1240,7 @@ json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, Jso
|
||||
// Format: YYYY-MM-DD (max 10 chars + null)
|
||||
char value[12];
|
||||
buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d", obj->year, obj->month, obj->day);
|
||||
set_json_icon_state_value(root, obj, "date", value, value, start_config);
|
||||
set_json_icon_state_value<const char *, const char *>(root, obj, "date", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
@@ -1290,7 +1300,7 @@ json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, Jso
|
||||
// Format: HH:MM:SS (8 chars + null)
|
||||
char value[12];
|
||||
buf_append_printf(value, sizeof(value), 0, "%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
|
||||
set_json_icon_state_value(root, obj, "time", value, value, start_config);
|
||||
set_json_icon_state_value<const char *, const char *>(root, obj, "time", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
@@ -1351,7 +1361,7 @@ json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *
|
||||
char value[24];
|
||||
buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour,
|
||||
obj->minute, obj->second);
|
||||
set_json_icon_state_value(root, obj, "datetime", value, value, start_config);
|
||||
set_json_icon_state_value<const char *, const char *>(root, obj, "datetime", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
@@ -2295,7 +2305,7 @@ json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, J
|
||||
JsonObject root = builder.root();
|
||||
|
||||
set_json_icon_state_value(root, obj, "update", json_state_str(update::update_state_to_string(obj->state)),
|
||||
obj->update_info.latest_version, start_config);
|
||||
obj->update_info.latest_version.c_str(), start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root[ESPHOME_F("current_version")] = obj->update_info.current_version;
|
||||
root[ESPHOME_F("title")] = obj->update_info.title;
|
||||
|
||||
@@ -593,7 +593,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
|
||||
|
||||
web_server_base::WebServerBase *base_;
|
||||
#ifdef USE_ESP32
|
||||
AsyncEventSource events_{"/events", this};
|
||||
AsyncEventSource events_{StringRef::from_lit("/events"), this};
|
||||
#elif USE_ARDUINO
|
||||
DeferredUpdateEventSourceList events_;
|
||||
#endif
|
||||
|
||||
@@ -322,7 +322,7 @@ class AsyncEventSource : public AsyncWebHandler {
|
||||
using connect_handler_t = std::function<void(AsyncEventSourceClient *)>;
|
||||
|
||||
public:
|
||||
AsyncEventSource(std::string url, esphome::web_server::WebServer *ws) : url_(std::move(url)), web_server_(ws) {}
|
||||
AsyncEventSource(StringRef url, esphome::web_server::WebServer *ws) : url_(url), web_server_(ws) {}
|
||||
~AsyncEventSource() override;
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
@@ -352,7 +352,7 @@ class AsyncEventSource : public AsyncWebHandler {
|
||||
// Cold path: move sessions from pending_sessions_ into sessions_ and greet each one.
|
||||
void __attribute__((noinline, cold)) adopt_pending_sessions_main_loop_();
|
||||
|
||||
std::string url_;
|
||||
StringRef url_; // Must outlive this object (string literal)
|
||||
// Main-loop only. Vector: SSE sessions are 1-5 connections, linear search beats set.
|
||||
std::vector<AsyncEventSourceResponse *> sessions_;
|
||||
// Httpd-task intake; guarded by pending_mutex_, gated by has_pending_sessions_.
|
||||
|
||||
@@ -352,7 +352,6 @@ CONF_DIRECTION = "direction"
|
||||
CONF_DIRECTION_COMMAND_TOPIC = "direction_command_topic"
|
||||
CONF_DIRECTION_OUTPUT = "direction_output"
|
||||
CONF_DIRECTION_STATE_TOPIC = "direction_state_topic"
|
||||
CONF_DISABLE_CRC = "disable_crc"
|
||||
CONF_DISABLED = "disabled"
|
||||
CONF_DISABLED_BY_DEFAULT = "disabled_by_default"
|
||||
CONF_DISCONNECT_DELAY = "disconnect_delay"
|
||||
|
||||
@@ -11,6 +11,19 @@
|
||||
#include <esp_chip_info.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_bootloader_desc.h>
|
||||
#include <esp_flash.h>
|
||||
#if __has_include(<esp_flash_chips/spi_flash_chip_generic.h>)
|
||||
#include <esp_flash_chips/spi_flash_chip_generic.h> // ESP-IDF 6
|
||||
#include <esp_flash_chips/spi_flash_chip_mxic.h>
|
||||
#else
|
||||
#include <spi_flash_chip_generic.h>
|
||||
#include <spi_flash_chip_mxic.h>
|
||||
#endif
|
||||
// Vendor flash drivers linked next to the generic one; sdkconfig defines each as 1 or not at all
|
||||
#define ESPHOME_FLASH_VENDOR_DRIVERS \
|
||||
(CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP + CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP + CONFIG_SPI_FLASH_SUPPORT_GD_CHIP + \
|
||||
CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP + CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP + CONFIG_SPI_FLASH_SUPPORT_TH_CHIP + \
|
||||
CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP)
|
||||
#endif
|
||||
#include "esphome/core/version.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -157,8 +170,25 @@ void Application::process_dump_config_() {
|
||||
esp_chip_info(&chip_info);
|
||||
ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100,
|
||||
chip_info.revision % 100, chip_info.cores);
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) && (!defined(USE_ESP32_MIN_CHIP_REVISION_SET) || !defined(USE_ESP32_SRAM1_AS_IRAM))
|
||||
static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced";
|
||||
[[maybe_unused]] static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced";
|
||||
#if ESPHOME_FLASH_VENDOR_DRIVERS > 0
|
||||
{
|
||||
// Only the driver in use earns its IRAM; with several linked at least one is idle
|
||||
const spi_flash_chip_t *flash_driver = esp_flash_default_chip->chip_drv;
|
||||
#if ESPHOME_FLASH_VENDOR_DRIVERS > 1
|
||||
constexpr bool idle_driver = true;
|
||||
#else
|
||||
const bool idle_driver = flash_driver == &esp_flash_chip_generic;
|
||||
#endif
|
||||
if (idle_driver) {
|
||||
const char *value = flash_driver->name;
|
||||
#ifdef CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP
|
||||
if (flash_driver == &esp_flash_chip_mxic_opi)
|
||||
value = "mxic_opi";
|
||||
#endif
|
||||
ESP_LOGW(TAG, "Set flash_chip: %s %s to save IRAM", value, ESP32_ADVANCED_PATH);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET)
|
||||
{
|
||||
|
||||
@@ -67,7 +67,7 @@ static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for qu
|
||||
class Application {
|
||||
public:
|
||||
#ifdef ESPHOME_NAME_ADD_MAC_SUFFIX
|
||||
// Called before Logger::pre_setup() — must not log (global_logger is not yet set).
|
||||
// Runs after Logger::pre_setup() (emitted at EARLY_INIT priority), so the app name is not set yet there.
|
||||
/// Pre-setup with MAC suffix: overwrites placeholder in mutable static buffers with actual MAC.
|
||||
void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len) {
|
||||
arch_init();
|
||||
@@ -87,7 +87,7 @@ class Application {
|
||||
this->friendly_name_ = StringRef(friendly_name, friendly_name_len);
|
||||
}
|
||||
#else
|
||||
// Called before Logger::pre_setup() — must not log (global_logger is not yet set).
|
||||
// Runs after Logger::pre_setup() (emitted at EARLY_INIT priority), so the app name is not set yet there.
|
||||
/// Pre-setup without MAC suffix: StringRef points directly at const string literals in flash.
|
||||
void pre_setup(const char *name, size_t name_len, const char *friendly_name, size_t friendly_name_len) {
|
||||
arch_init();
|
||||
|
||||
+5
-10
@@ -6,18 +6,13 @@ namespace esphome {
|
||||
constinit const Color Color::BLACK(0, 0, 0, 0);
|
||||
constinit const Color Color::WHITE(255, 255, 255, 255);
|
||||
|
||||
Color Color::gradient(const Color &to_color, uint8_t amnt) {
|
||||
uint8_t inv = 255 - amnt;
|
||||
Color new_color;
|
||||
new_color.r = (uint16_t(this->r) * inv + uint16_t(to_color.r) * amnt) / 255;
|
||||
new_color.g = (uint16_t(this->g) * inv + uint16_t(to_color.g) * amnt) / 255;
|
||||
new_color.b = (uint16_t(this->b) * inv + uint16_t(to_color.b) * amnt) / 255;
|
||||
new_color.w = (uint16_t(this->w) * inv + uint16_t(to_color.w) * amnt) / 255;
|
||||
return new_color;
|
||||
Color Color::gradient(const Color &to_color, uint8_t amnt) const {
|
||||
return Color(blend_channel(this->r, to_color.r, amnt), blend_channel(this->g, to_color.g, amnt),
|
||||
blend_channel(this->b, to_color.b, amnt), blend_channel(this->w, to_color.w, amnt));
|
||||
}
|
||||
|
||||
Color Color::fade_to_white(uint8_t amnt) { return this->gradient(Color::WHITE, amnt); }
|
||||
Color Color::fade_to_white(uint8_t amnt) const { return this->gradient(Color::WHITE, amnt); }
|
||||
|
||||
Color Color::fade_to_black(uint8_t amnt) { return this->gradient(Color::BLACK, amnt); }
|
||||
Color Color::fade_to_black(uint8_t amnt) const { return this->gradient(Color::BLACK, amnt); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -174,9 +174,15 @@ struct Color {
|
||||
uint8_t((uint16_t(b) * 255U / max_rgb)), w);
|
||||
}
|
||||
|
||||
Color gradient(const Color &to_color, uint8_t amnt);
|
||||
Color fade_to_white(uint8_t amnt);
|
||||
Color fade_to_black(uint8_t amnt);
|
||||
/// One channel of gradient(): from at amnt 0 to to at amnt 255. Inline so a
|
||||
/// per pixel loop can blend without a call; gradient() itself stays out of
|
||||
/// line so the light effects and fade_to_*() share one copy.
|
||||
static inline uint8_t blend_channel(uint8_t from, uint8_t to, uint8_t amnt) ESPHOME_ALWAYS_INLINE {
|
||||
return (uint16_t(from) * (255 - amnt) + uint16_t(to) * amnt) / 255;
|
||||
}
|
||||
Color gradient(const Color &to_color, uint8_t amnt) const;
|
||||
Color fade_to_white(uint8_t amnt) const;
|
||||
Color fade_to_black(uint8_t amnt) const;
|
||||
|
||||
Color lighten(uint8_t delta) { return *this + delta; }
|
||||
Color darken(uint8_t delta) { return *this - delta; }
|
||||
|
||||
@@ -400,6 +400,7 @@
|
||||
#define USE_SENDSPIN_CONTROLLER
|
||||
#define USE_SENDSPIN_METADATA
|
||||
#define USE_SENDSPIN_PLAYER
|
||||
#define USE_SENDSPIN_SWITCH
|
||||
#define USE_SENDSPIN_VISUALIZER
|
||||
#define USE_SENDSPIN_PORT 8928 // NOLINT
|
||||
#define USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
@@ -536,7 +537,7 @@
|
||||
// rp2/__init__.py codegen also defines USE_RP2040 as a back-compat alias
|
||||
// for external custom components that may still test for it.
|
||||
#ifdef USE_RP2
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(6, 0, 0)
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(6, 1, 0)
|
||||
#define USE_RP2_CRASH_HANDLER
|
||||
#define USE_HTTP_REQUEST_RESPONSE
|
||||
#define USE_I2C
|
||||
|
||||
@@ -9,16 +9,19 @@ byte-identical to PlatformIO's output:
|
||||
Flash: [=== ] 48.4% (used 888511 bytes from 1835008 bytes)
|
||||
|
||||
The format matches ``script/ci_memory_impact_extract.py`` so CI memory
|
||||
analysis works unchanged on native ESP-IDF builds. RAM total is the
|
||||
DRAM region size from the linker map; Flash total is taken from
|
||||
analysis works unchanged on native ESP-IDF builds. RAM usage comes from
|
||||
the DRAM (or unified DIRAM) region of the linker map. Flash used is the
|
||||
exact image size matching the ``Total image size`` line: json2
|
||||
``total_size`` when present, otherwise derived from the ELF (see
|
||||
``_image_size_from_elf``). Flash total is taken from
|
||||
``partitions.csv`` using PlatformIO's rule (first app partition whose
|
||||
subtype is ``factory`` or ``ota_0``; see
|
||||
``platform-espressif32/builder/main.py::_update_max_upload_size``).
|
||||
|
||||
Structured size data is produced at link time by a CMake POST_BUILD
|
||||
custom command (see ``build_gen/espidf.py``) which writes
|
||||
``esp_idf_size.json`` next to the ELF. We read that file here rather
|
||||
than re-running ``esp_idf_size`` from Python.
|
||||
``esp_idf_size.json`` (``--format=json2``, a per-memory-type summary)
|
||||
next to the ELF; we read that rather than re-running ``esp_idf_size``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,6 +30,7 @@ import csv
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import struct
|
||||
|
||||
from esphome.build_helpers.size_summary import print_size_line
|
||||
|
||||
@@ -69,11 +73,43 @@ def _find_app_partition_size(partitions_csv: Path) -> int:
|
||||
raise ValueError(f"No app+factory or app+ota_0 partition in {partitions_csv}")
|
||||
|
||||
|
||||
def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
|
||||
def _image_size_from_elf(elf: Path) -> int:
|
||||
"""Sum the allocated PROGBITS section sizes from an ELF32 file.
|
||||
|
||||
Matches ``esp_idf_size.ng.memorymap._get_image_size`` byte for byte;
|
||||
esptool's ``ELFFile`` filters sections differently and would not.
|
||||
Raises ``ValueError`` for anything but a well-formed ELF32 LE file.
|
||||
"""
|
||||
with elf.open("rb") as f:
|
||||
header = f.read(52) # ELF32 header
|
||||
if len(header) < 52 or header[:6] != b"\x7fELF\x01\x01":
|
||||
raise ValueError(f"{elf} is not a 32-bit little-endian ELF")
|
||||
(e_shoff,) = struct.unpack_from("<I", header, 0x20) # e_shoff
|
||||
e_shentsize, e_shnum = struct.unpack_from("<HH", header, 0x2E)
|
||||
if e_shentsize < 40: # sizeof(Elf32_Shdr)
|
||||
raise ValueError(f"{elf} has an invalid section header size")
|
||||
f.seek(e_shoff)
|
||||
table = f.read(e_shnum * e_shentsize)
|
||||
if len(table) < e_shnum * e_shentsize:
|
||||
raise ValueError(f"{elf} has a truncated section header table")
|
||||
total = 0
|
||||
for off in range(0, e_shnum * e_shentsize, e_shentsize):
|
||||
sh_type, sh_flags = struct.unpack_from("<II", table, off + 4)
|
||||
(sh_size,) = struct.unpack_from("<I", table, off + 20)
|
||||
if sh_type == 1 and sh_flags & 0x2: # SHT_PROGBITS with SHF_ALLOC
|
||||
total += sh_size
|
||||
if total == 0:
|
||||
# A used-0-bytes Flash line would read as a real measurement
|
||||
raise ValueError(f"{elf} has no allocated PROGBITS sections")
|
||||
return total
|
||||
|
||||
|
||||
def print_summary(size_json: Path, partitions_csv: Path, firmware_elf: Path) -> None:
|
||||
"""Print PlatformIO-shaped RAM and Flash one-liners.
|
||||
|
||||
Failures are non-fatal: the build has already succeeded, we just couldn't
|
||||
summarize. Logs the cause at debug level.
|
||||
summarize. Anomalies (missing region, unreadable ELF) warn; expected
|
||||
optional inputs (no size json, no partitions.csv) log at debug.
|
||||
"""
|
||||
if not size_json.is_file():
|
||||
_LOGGER.debug("Skipping size summary: %s not found", size_json)
|
||||
@@ -83,20 +119,49 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
_LOGGER.debug("Skipping size summary: %s", e)
|
||||
return
|
||||
|
||||
memory_types = data.get("memory_types", {})
|
||||
ram_region = memory_types.get("DRAM") or memory_types.get("DIRAM") or {}
|
||||
ram_used = ram_region.get("used")
|
||||
ram_total = ram_region.get("size")
|
||||
if ram_total and ram_used is not None:
|
||||
print_size_line("RAM", ram_used, ram_total)
|
||||
|
||||
image_size = data.get("image_size")
|
||||
if image_size is None or partitions_csv is None:
|
||||
if not isinstance(data, dict):
|
||||
_LOGGER.warning("Skipping size summary: unexpected json shape in %s", size_json)
|
||||
return
|
||||
|
||||
layout = data.get("layout")
|
||||
regions = {
|
||||
entry.get("name"): entry
|
||||
for entry in (layout if isinstance(layout, list) else [])
|
||||
if isinstance(entry, dict)
|
||||
}
|
||||
# Every chip has a DRAM or DIRAM region, so a warning here usually
|
||||
# means the esp_idf_size json schema changed
|
||||
ram_region = regions.get("DRAM") or regions.get("DIRAM")
|
||||
if ram_region is None:
|
||||
_LOGGER.warning("Skipping RAM summary: no DRAM/DIRAM region in %s", size_json)
|
||||
elif (
|
||||
isinstance(ram_total := ram_region.get("total"), int)
|
||||
and ram_total > 0
|
||||
and isinstance(ram_used := ram_region.get("used"), int)
|
||||
):
|
||||
print_size_line("RAM", ram_used, ram_total)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Skipping RAM summary: unusable region %s in %s", ram_region, size_json
|
||||
)
|
||||
|
||||
# esp-idf-size >= 2.1 (IDF >= 6.0) reports the exact image size in
|
||||
# json2; older 1.x omits it, so derive the same figure from the ELF.
|
||||
flash_used = data.get("total_size")
|
||||
if not (isinstance(flash_used, int) and flash_used > 0):
|
||||
_LOGGER.debug("No total_size in %s, deriving from %s", size_json, firmware_elf)
|
||||
try:
|
||||
flash_used = _image_size_from_elf(firmware_elf)
|
||||
except (OSError, ValueError) as e:
|
||||
# The ELF must be present and well formed after a successful build
|
||||
_LOGGER.warning("Skipping Flash summary: %s", e)
|
||||
return
|
||||
try:
|
||||
app_size = _find_app_partition_size(partitions_csv)
|
||||
except ValueError as e:
|
||||
except (OSError, ValueError) as e:
|
||||
_LOGGER.debug("Skipping Flash summary: %s", e)
|
||||
return
|
||||
print_size_line("Flash", image_size, app_size)
|
||||
if app_size <= 0:
|
||||
_LOGGER.debug("Skipping Flash summary: app partition size is 0")
|
||||
return
|
||||
print_size_line("Flash", flash_used, app_size)
|
||||
|
||||
@@ -542,7 +542,7 @@ def run_compile(config, verbose: bool) -> int:
|
||||
if rc == 0:
|
||||
size_json = CORE.relative_build_path("build", "esp_idf_size.json")
|
||||
partitions = CORE.relative_build_path("partitions.csv")
|
||||
print_summary(size_json, partitions if partitions.is_file() else None)
|
||||
print_summary(size_json, partitions, get_built_elf_path())
|
||||
return rc
|
||||
|
||||
|
||||
@@ -579,6 +579,16 @@ def get_ota_firmware_path() -> Path:
|
||||
return build_dir / "firmware.ota.bin"
|
||||
|
||||
|
||||
def get_built_elf_path() -> Path:
|
||||
"""Path to the ELF idf.py writes directly, ``<build>/<name>.elf``.
|
||||
|
||||
Exists as soon as the build finishes, unlike the ``firmware.elf``
|
||||
copy that ``create_elf_copy`` makes later.
|
||||
"""
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
return build_dir / f"{CORE.name}.elf"
|
||||
|
||||
|
||||
def get_elf_path() -> Path:
|
||||
"""Get the path to the firmware ELF file.
|
||||
|
||||
@@ -706,8 +716,7 @@ def create_elf_copy() -> bool:
|
||||
"download ELF" link requests the literal filename ``firmware.elf``
|
||||
(PlatformIO convention), so copy it to that name.
|
||||
"""
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
src_elf = build_dir / f"{CORE.name}.elf"
|
||||
src_elf = get_built_elf_path()
|
||||
dst_elf = get_elf_path()
|
||||
|
||||
if not src_elf.is_file():
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user