1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-11 10:12:38 +00:00

Compare commits

...

29 Commits

Author SHA1 Message Date
J. Nick Koston
38bba3f5a2 [scheduler] Reduce set_timer_common_ hot path size by 25% (#13899) 2026-02-11 16:42:13 +13:00
J. Nick Koston
225c13326a [core] Extract dump_config from Application::loop() hot path (#13900) 2026-02-11 16:41:07 +13:00
J. Nick Koston
5281fd3273 [api] Extract cold code from APIConnection::loop() hot path (#13901) 2026-02-11 16:30:34 +13:00
J. Nick Koston
e3bafc1b45 [esp32_ble] Extract state transitions from ESP32BLE::loop() hot path (#13903)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 16:29:29 +13:00
Thomas Rupprecht
42bc0994f1 [rtttl] Code Improvements (#13653)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-02-10 22:10:29 -05:00
J. Nick Koston
58659e4893 [mdns] Throttle MDNS.update() polling on ESP8266 and RP2040 (#13917) 2026-02-10 18:48:13 -06:00
Jonathan Swoboda
b4707344d3 [esp32] Upgrade uv to 0.10.1 and increase HTTP retries (#13918)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 00:44:12 +00:00
Jonathan Swoboda
548b7e5dab [esp32] Fix ESP32-P4 test: replace stale esp_hosted component ref (#13920)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 00:04:12 +00:00
Jesse Hills
b9c2be8228 Merge branch 'release' into dev 2026-02-11 11:13:33 +13:00
Jesse Hills
fb2f0ce62f Merge pull request #13915 from esphome/bump-2026.1.5
2026.1.5
2026-02-11 11:13:08 +13:00
J. Nick Koston
d152438335 [libretiny] Update LibreTiny to v1.12.1 (#13851) 2026-02-10 20:07:09 +00:00
J. Nick Koston
868a2151e3 [web_server_idf] Reduce heap allocations by using stack buffers (#13549) 2026-02-10 13:56:12 -06:00
J. Nick Koston
c65d3a0072 [mqtt] Add zero-allocation topic getters to MQTT_COMPONENT_CUSTOM_TOPIC macro (#13811) 2026-02-10 13:55:16 -06:00
J. Nick Koston
e2fad9a6c9 [sprinkler] Convert state and request origin strings to PROGMEM_STRING_TABLE (#13806) 2026-02-10 13:55:01 -06:00
J. Nick Koston
5365faa877 [debug] Move ESP8266 switch tables to flash with PROGMEM_STRING_TABLE (#13813) 2026-02-10 13:54:48 -06:00
J. Nick Koston
86feb4e27a [rtttl] Convert state_to_string to PROGMEM_STRING_TABLE (#13807)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 13:54:37 -06:00
J. Nick Koston
2a6d9d6325 [mqtt] Avoid heap allocation in on_log by using const char* publish overload (#13809) 2026-02-10 13:54:22 -06:00
J. Nick Koston
727bb27611 [bmp3xx_base/bmp581_base] Convert oversampling and IIR filter strings to PROGMEM_STRING_TABLE (#13808) 2026-02-10 13:54:07 -06:00
J. Nick Koston
c03abcdb86 [http_request] Reduce heap allocations in update check by parsing JSON directly from buffer (#13588) 2026-02-10 13:53:53 -06:00
Jesse Hills
a99f75ca71 Bump version to 2026.1.5 2026-02-11 08:45:06 +13:00
Sean Kelly
4168e8c30d [aqi] Fix AQI calculation for specific pm2.5 or pm10 readings (#13770) 2026-02-11 08:45:06 +13:00
J. Nick Koston
1a6c67f92e [ssd1306_base] Move switch tables to PROGMEM with lookup tables (#13814) 2026-02-10 13:45:03 -06:00
Jonathan Swoboda
1f761902b6 [esp32] Set UV_CACHE_DIR inside data dir so Clean All clears it (#13888)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 07:48:20 +13:00
Clyde Stubbs
0b047c334d [lvgl] Fix crash with unconfigured top_layer (#13846) 2026-02-11 07:24:32 +13:00
tomaszduda23
a5dc4b0fce [nrf52,logger] fix printk (#13874) 2026-02-11 07:24:32 +13:00
J. Nick Koston
c1455ccc29 [dashboard] Close WebSocket after process exit to prevent zombie connections (#13834) 2026-02-11 07:24:32 +13:00
Jonathan Swoboda
438a0c4289 [ota] Fix CLI upload option shown when only http_request platform configured (#13784)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 07:24:32 +13:00
Jonathan Swoboda
9eee4c9924 [core] Add capacity check to register_component_ (#13778)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 07:24:32 +13:00
Jas Strong
eea7e9edff [rd03d] Revert incorrect field order swap (#13769)
Co-authored-by: jas <jas@asspa.in>
2026-02-11 07:24:32 +13:00
44 changed files with 1147 additions and 818 deletions

View File

@@ -1 +1 @@
8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695 74867fc82764102ce1275ea2bc43e3aeee7619679537c6db61114a33342bb4c7

View File

@@ -23,7 +23,7 @@ RUN if command -v apk > /dev/null; then \
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14 RUN pip install --no-cache-dir -U pip uv==0.10.1
COPY requirements.txt / COPY requirements.txt /

View File

@@ -219,35 +219,8 @@ void APIConnection::loop() {
this->process_batch_(); this->process_batch_();
} }
switch (this->active_iterator_) { if (this->active_iterator_ != ActiveIterator::NONE) {
case ActiveIterator::LIST_ENTITIES: this->process_active_iterator_();
if (this->iterator_storage_.list_entities.completed()) {
this->destroy_active_iterator_();
if (this->flags_.state_subscription) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
} else {
this->process_iterator_batch_(this->iterator_storage_.list_entities);
}
break;
case ActiveIterator::INITIAL_STATE:
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
break;
case ActiveIterator::NONE:
break;
} }
if (this->flags_.sent_ping) { if (this->flags_.sent_ping) {
@@ -283,6 +256,49 @@ void APIConnection::loop() {
#endif #endif
} }
void APIConnection::process_active_iterator_() {
// Caller ensures active_iterator_ != NONE
if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) {
if (this->iterator_storage_.list_entities.completed()) {
this->destroy_active_iterator_();
if (this->flags_.state_subscription) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
} else {
this->process_iterator_batch_(this->iterator_storage_.list_entities);
}
} else { // INITIAL_STATE
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
}
}
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
size_t initial_size = this->deferred_batch_.size();
size_t max_batch = this->get_max_batch_size_();
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
iterator.advance();
}
// If the batch is full, process it immediately
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
if (this->deferred_batch_.size() >= max_batch) {
this->process_batch_();
}
}
bool APIConnection::send_disconnect_response_() { bool APIConnection::send_disconnect_response_() {
// remote initiated disconnect_client // remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response // don't close yet, we still need to send the disconnect response

View File

@@ -15,6 +15,10 @@
#include <limits> #include <limits>
#include <vector> #include <vector>
namespace esphome {
class ComponentIterator;
} // namespace esphome
namespace esphome::api { namespace esphome::api {
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
@@ -366,20 +370,13 @@ class APIConnection final : public APIServerConnectionBase {
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY; return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
} }
// Helper method to process multiple entities from an iterator in a batch // Process active iterator (list_entities/initial_state) during connection setup.
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) { // Extracted from loop() — only runs during initial handshake, NONE in steady state.
size_t initial_size = this->deferred_batch_.size(); void __attribute__((noinline)) process_active_iterator_();
size_t max_batch = this->get_max_batch_size_();
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
iterator.advance();
}
// If the batch is full, process it immediately // Helper method to process multiple entities from an iterator in a batch.
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_() // Takes ComponentIterator base class reference to avoid duplicate template instantiations.
if (this->deferred_batch_.size() >= max_batch) { void process_iterator_batch_(ComponentIterator &iterator);
this->process_batch_();
}
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);

View File

@@ -94,7 +94,6 @@ class ListEntitiesIterator : public ComponentIterator {
bool on_update(update::UpdateEntity *entity) override; bool on_update(update::UpdateEntity *entity) override;
#endif #endif
bool on_end() override; bool on_end() override;
bool completed() { return this->state_ == IteratorState::NONE; }
protected: protected:
APIConnection *client_; APIConnection *client_;

View File

@@ -88,7 +88,6 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool on_update(update::UpdateEntity *entity) override; bool on_update(update::UpdateEntity *entity) override;
#endif #endif
bool completed() { return this->state_ == IteratorState::NONE; }
protected: protected:
APIConnection *client_; APIConnection *client_;

View File

@@ -159,6 +159,10 @@ BK72XX_BOARD_PINS = {
"A0": 23, "A0": 23,
}, },
"cbu": { "cbu": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA": 21, "WIRE1_SDA": 21,
"WIRE2_SCL": 0, "WIRE2_SCL": 0,
@@ -227,6 +231,10 @@ BK72XX_BOARD_PINS = {
"A0": 23, "A0": 23,
}, },
"generic-bk7231t-qfn32-tuya": { "generic-bk7231t-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA": 21, "WIRE1_SDA": 21,
"WIRE2_SCL": 0, "WIRE2_SCL": 0,
@@ -295,6 +303,10 @@ BK72XX_BOARD_PINS = {
"A0": 23, "A0": 23,
}, },
"generic-bk7231n-qfn32-tuya": { "generic-bk7231n-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA": 21, "WIRE1_SDA": 21,
"WIRE2_SCL": 0, "WIRE2_SCL": 0,
@@ -485,8 +497,7 @@ BK72XX_BOARD_PINS = {
}, },
"cb3s": { "cb3s": {
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA_0": 21, "WIRE1_SDA": 21,
"WIRE1_SDA_1": 21,
"SERIAL1_RX": 10, "SERIAL1_RX": 10,
"SERIAL1_TX": 11, "SERIAL1_TX": 11,
"SERIAL2_TX": 0, "SERIAL2_TX": 0,
@@ -647,6 +658,10 @@ BK72XX_BOARD_PINS = {
"A0": 23, "A0": 23,
}, },
"generic-bk7252": { "generic-bk7252": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA": 21, "WIRE1_SDA": 21,
"WIRE2_SCL": 0, "WIRE2_SCL": 0,
@@ -1096,6 +1111,10 @@ BK72XX_BOARD_PINS = {
"A0": 23, "A0": 23,
}, },
"cb3se": { "cb3se": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL": 0, "WIRE2_SCL": 0,
"WIRE2_SDA": 1, "WIRE2_SDA": 1,
"SERIAL1_RX": 10, "SERIAL1_RX": 10,

View File

@@ -6,8 +6,9 @@
*/ */
#include "bmp3xx_base.h" #include "bmp3xx_base.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes> #include <cinttypes>
namespace esphome { namespace esphome {
@@ -26,46 +27,18 @@ static const LogString *chip_type_to_str(uint8_t chip_type) {
} }
} }
// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "");
static const LogString *oversampling_to_str(Oversampling oversampling) { static const LogString *oversampling_to_str(Oversampling oversampling) {
switch (oversampling) { return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
case Oversampling::OVERSAMPLING_NONE:
return LOG_STR("None");
case Oversampling::OVERSAMPLING_X2:
return LOG_STR("2x");
case Oversampling::OVERSAMPLING_X4:
return LOG_STR("4x");
case Oversampling::OVERSAMPLING_X8:
return LOG_STR("8x");
case Oversampling::OVERSAMPLING_X16:
return LOG_STR("16x");
case Oversampling::OVERSAMPLING_X32:
return LOG_STR("32x");
default:
return LOG_STR("");
}
} }
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *iir_filter_to_str(IIRFilter filter) { static const LogString *iir_filter_to_str(IIRFilter filter) {
switch (filter) { return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
case IIRFilter::IIR_FILTER_OFF:
return LOG_STR("OFF");
case IIRFilter::IIR_FILTER_2:
return LOG_STR("2x");
case IIRFilter::IIR_FILTER_4:
return LOG_STR("4x");
case IIRFilter::IIR_FILTER_8:
return LOG_STR("8x");
case IIRFilter::IIR_FILTER_16:
return LOG_STR("16x");
case IIRFilter::IIR_FILTER_32:
return LOG_STR("32x");
case IIRFilter::IIR_FILTER_64:
return LOG_STR("64x");
case IIRFilter::IIR_FILTER_128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
} }
void BMP3XXComponent::setup() { void BMP3XXComponent::setup() {

View File

@@ -11,57 +11,26 @@
*/ */
#include "bmp581_base.h" #include "bmp581_base.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::bmp581_base { namespace esphome::bmp581_base {
static const char *const TAG = "bmp581"; static const char *const TAG = "bmp581";
// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *oversampling_to_str(Oversampling oversampling) { static const LogString *oversampling_to_str(Oversampling oversampling) {
switch (oversampling) { return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
case Oversampling::OVERSAMPLING_NONE:
return LOG_STR("None");
case Oversampling::OVERSAMPLING_X2:
return LOG_STR("2x");
case Oversampling::OVERSAMPLING_X4:
return LOG_STR("4x");
case Oversampling::OVERSAMPLING_X8:
return LOG_STR("8x");
case Oversampling::OVERSAMPLING_X16:
return LOG_STR("16x");
case Oversampling::OVERSAMPLING_X32:
return LOG_STR("32x");
case Oversampling::OVERSAMPLING_X64:
return LOG_STR("64x");
case Oversampling::OVERSAMPLING_X128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
} }
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *iir_filter_to_str(IIRFilter filter) { static const LogString *iir_filter_to_str(IIRFilter filter) {
switch (filter) { return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
case IIRFilter::IIR_FILTER_OFF:
return LOG_STR("OFF");
case IIRFilter::IIR_FILTER_2:
return LOG_STR("2x");
case IIRFilter::IIR_FILTER_4:
return LOG_STR("4x");
case IIRFilter::IIR_FILTER_8:
return LOG_STR("8x");
case IIRFilter::IIR_FILTER_16:
return LOG_STR("16x");
case IIRFilter::IIR_FILTER_32:
return LOG_STR("32x");
case IIRFilter::IIR_FILTER_64:
return LOG_STR("64x");
case IIRFilter::IIR_FILTER_128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
} }
void BMP581Component::dump_config() { void BMP581Component::dump_config() {

View File

@@ -1,6 +1,7 @@
#include "debug_component.h" #include "debug_component.h"
#ifdef USE_ESP8266 #ifdef USE_ESP8266
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <Esp.h> #include <Esp.h>
extern "C" { extern "C" {
@@ -19,27 +20,38 @@ namespace debug {
static const char *const TAG = "debug"; static const char *const TAG = "debug";
// PROGMEM string table for reset reasons, indexed by reason code (0-6), with "Unknown" as fallback
// clang-format off
PROGMEM_STRING_TABLE(ResetReasonStrings,
"Power On", // 0 = REASON_DEFAULT_RST
"Hardware Watchdog", // 1 = REASON_WDT_RST
"Exception", // 2 = REASON_EXCEPTION_RST
"Software Watchdog", // 3 = REASON_SOFT_WDT_RST
"Software/System restart", // 4 = REASON_SOFT_RESTART
"Deep-Sleep Wake", // 5 = REASON_DEEP_SLEEP_AWAKE
"External System", // 6 = REASON_EXT_SYS_RST
"Unknown" // 7 = fallback
);
// clang-format on
static_assert(REASON_DEFAULT_RST == 0, "Reset reason enum values must match table indices");
static_assert(REASON_WDT_RST == 1, "Reset reason enum values must match table indices");
static_assert(REASON_EXCEPTION_RST == 2, "Reset reason enum values must match table indices");
static_assert(REASON_SOFT_WDT_RST == 3, "Reset reason enum values must match table indices");
static_assert(REASON_SOFT_RESTART == 4, "Reset reason enum values must match table indices");
static_assert(REASON_DEEP_SLEEP_AWAKE == 5, "Reset reason enum values must match table indices");
static_assert(REASON_EXT_SYS_RST == 6, "Reset reason enum values must match table indices");
// PROGMEM string table for flash chip modes, indexed by mode code (0-3), with "UNKNOWN" as fallback
PROGMEM_STRING_TABLE(FlashModeStrings, "QIO", "QOUT", "DIO", "DOUT", "UNKNOWN");
static_assert(FM_QIO == 0, "Flash mode enum values must match table indices");
static_assert(FM_QOUT == 1, "Flash mode enum values must match table indices");
static_assert(FM_DIO == 2, "Flash mode enum values must match table indices");
static_assert(FM_DOUT == 3, "Flash mode enum values must match table indices");
// Get reset reason string from reason code (no heap allocation) // Get reset reason string from reason code (no heap allocation)
// Returns LogString* pointing to flash (PROGMEM) on ESP8266 // Returns LogString* pointing to flash (PROGMEM) on ESP8266
static const LogString *get_reset_reason_str(uint32_t reason) { static const LogString *get_reset_reason_str(uint32_t reason) {
switch (reason) { return ResetReasonStrings::get_log_str(static_cast<uint8_t>(reason), ResetReasonStrings::LAST_INDEX);
case REASON_DEFAULT_RST:
return LOG_STR("Power On");
case REASON_WDT_RST:
return LOG_STR("Hardware Watchdog");
case REASON_EXCEPTION_RST:
return LOG_STR("Exception");
case REASON_SOFT_WDT_RST:
return LOG_STR("Software Watchdog");
case REASON_SOFT_RESTART:
return LOG_STR("Software/System restart");
case REASON_DEEP_SLEEP_AWAKE:
return LOG_STR("Deep-Sleep Wake");
case REASON_EXT_SYS_RST:
return LOG_STR("External System");
default:
return LOG_STR("Unknown");
}
} }
// Size for core version hex buffer // Size for core version hex buffer
@@ -92,23 +104,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data(); char *buf = buffer.data();
const LogString *flash_mode; const LogString *flash_mode = FlashModeStrings::get_log_str(
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) static_cast<uint8_t>(ESP.getFlashChipMode()), // NOLINT(readability-static-accessed-through-instance)
case FM_QIO: FlashModeStrings::LAST_INDEX);
flash_mode = LOG_STR("QIO");
break;
case FM_QOUT:
flash_mode = LOG_STR("QOUT");
break;
case FM_DIO:
flash_mode = LOG_STR("DIO");
break;
case FM_DOUT:
flash_mode = LOG_STR("DOUT");
break;
default:
flash_mode = LOG_STR("UNKNOWN");
}
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance) uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance) uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,

View File

@@ -1436,10 +1436,6 @@ async def to_code(config):
CORE.relative_internal_path(".espressif") CORE.relative_internal_path(".espressif")
) )
# Set the uv cache inside the data dir so "Clean All" clears it.
# Avoids persistent corrupted cache from mid-stream download failures.
os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache"))
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")

View File

@@ -369,42 +369,9 @@ bool ESP32BLE::ble_dismantle_() {
} }
void ESP32BLE::loop() { void ESP32BLE::loop() {
switch (this->state_) { if (this->state_ != BLE_COMPONENT_STATE_ACTIVE) {
case BLE_COMPONENT_STATE_OFF: this->loop_handle_state_transition_not_active_();
case BLE_COMPONENT_STATE_DISABLED: return;
return;
case BLE_COMPONENT_STATE_DISABLE: {
ESP_LOGD(TAG, "Disabling");
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
ble_event_handler->ble_before_disabled_event_handler();
}
#endif
if (!ble_dismantle_()) {
ESP_LOGE(TAG, "Could not be dismantled");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_DISABLED;
return;
}
case BLE_COMPONENT_STATE_ENABLE: {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
if (!ble_setup_()) {
ESP_LOGE(TAG, "Could not be set up");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
return;
}
case BLE_COMPONENT_STATE_ACTIVE:
break;
} }
BLEEvent *ble_event = this->ble_events_.pop(); BLEEvent *ble_event = this->ble_events_.pop();
@@ -520,6 +487,37 @@ void ESP32BLE::loop() {
} }
} }
void ESP32BLE::loop_handle_state_transition_not_active_() {
// Caller ensures state_ != ACTIVE
if (this->state_ == BLE_COMPONENT_STATE_DISABLE) {
ESP_LOGD(TAG, "Disabling");
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
ble_event_handler->ble_before_disabled_event_handler();
}
#endif
if (!ble_dismantle_()) {
ESP_LOGE(TAG, "Could not be dismantled");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_DISABLED;
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
if (!ble_setup_()) {
ESP_LOGE(TAG, "Could not be set up");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
}
}
// Helper function to load new event data based on type // Helper function to load new event data based on type
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
event->load_gap_event(e, p); event->load_gap_event(e, p);

View File

@@ -155,6 +155,10 @@ class ESP32BLE : public Component {
#endif #endif
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
// Handle DISABLE and ENABLE transitions when not in the ACTIVE state.
// Other non-ACTIVE states (e.g. OFF, DISABLED) are currently treated as no-ops.
void __attribute__((noinline)) loop_handle_state_transition_not_active_();
bool ble_setup_(); bool ble_setup_();
bool ble_dismantle_(); bool ble_dismantle_();
bool ble_pre_setup_(); bool ble_pre_setup_();

View File

@@ -90,16 +90,14 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN; UPDATE_RETURN;
} }
size_t read_index = container->get_bytes_read(); size_t read_index = container->get_bytes_read();
size_t content_length = container->content_length;
container->end();
container.reset(); // Release ownership of the container's shared_ptr
bool valid = false; bool valid = false;
{ // Ensures the response string falls out of scope and deallocates before the task ends { // Scope to ensure JsonDocument is destroyed before deallocating buffer
std::string response((char *) data, read_index); valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
allocator.deallocate(data, container->content_length);
container->end();
container.reset(); // Release ownership of the container's shared_ptr
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() || if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
!root[ESPHOME_F("builds")].is<JsonArray>()) { !root[ESPHOME_F("builds")].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields"); ESP_LOGE(TAG, "Manifest does not contain required fields");
@@ -137,6 +135,7 @@ void HttpRequestUpdate::update_task(void *params) {
return false; return false;
}); });
} }
allocator.deallocate(data, content_length);
if (!valid) { if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
@@ -157,17 +156,12 @@ void HttpRequestUpdate::update_task(void *params) {
} }
} }
{ // Ensures the current version string falls out of scope and deallocates before the task ends
std::string current_version;
#ifdef ESPHOME_PROJECT_VERSION #ifdef ESPHOME_PROJECT_VERSION
current_version = ESPHOME_PROJECT_VERSION; this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
#else #else
current_version = ESPHOME_VERSION; this_update->update_info_.current_version = ESPHOME_VERSION;
#endif #endif
this_update->update_info_.current_version = current_version;
}
bool trigger_update_available = false; bool trigger_update_available = false;
if (this_update->update_info_.latest_version.empty() || if (this_update->update_info_.latest_version.empty() ||

View File

@@ -25,8 +25,13 @@ std::string build_json(const json_build_t &f) {
} }
bool parse_json(const std::string &data, const json_parse_t &f) { bool parse_json(const std::string &data, const json_parse_t &f) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size(), f);
}
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size()); JsonDocument doc = parse_json(data, len);
if (doc.overflowed() || doc.isNull()) if (doc.overflowed() || doc.isNull())
return false; return false;
return f(doc.as<JsonObject>()); return f(doc.as<JsonObject>());

View File

@@ -50,6 +50,8 @@ std::string build_json(const json_build_t &f);
/// Parse a JSON string and run the provided json parse function if it's valid. /// Parse a JSON string and run the provided json parse function if it's valid.
bool parse_json(const std::string &data, const json_parse_t &f); bool parse_json(const std::string &data, const json_parse_t &f);
/// Parse JSON from raw bytes and run the provided json parse function if it's valid.
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f);
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error) /// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
JsonDocument parse_json(const uint8_t *data, size_t len); JsonDocument parse_json(const uint8_t *data, size_t len);

View File

@@ -193,14 +193,14 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters. # The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays. # Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = { ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"), "dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
"latest": ( "latest": (
cv.Version(1, 11, 0), cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0", "https://github.com/libretiny-eu/libretiny.git#v1.12.1",
), ),
"recommended": ( "recommended": (
cv.Version(1, 11, 0), cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0", "https://github.com/libretiny-eu/libretiny.git#v1.12.1",
), ),
} }

View File

@@ -154,28 +154,26 @@ LN882X_BOARD_PINS = {
"A7": 21, "A7": 21,
}, },
"wb02a": { "wb02a": {
"WIRE0_SCL_0": 7, "WIRE0_SCL_0": 1,
"WIRE0_SCL_1": 5, "WIRE0_SCL_1": 2,
"WIRE0_SCL_2": 3, "WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 10, "WIRE0_SCL_3": 4,
"WIRE0_SCL_4": 2, "WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 1, "WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 4, "WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 5, "WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 9, "WIRE0_SCL_8": 24,
"WIRE0_SCL_9": 24, "WIRE0_SCL_9": 25,
"WIRE0_SCL_10": 25, "WIRE0_SDA_0": 1,
"WIRE0_SDA_0": 7, "WIRE0_SDA_1": 2,
"WIRE0_SDA_1": 5,
"WIRE0_SDA_2": 3, "WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 10, "WIRE0_SDA_3": 4,
"WIRE0_SDA_4": 2, "WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 1, "WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 4, "WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 5, "WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 9, "WIRE0_SDA_8": 24,
"WIRE0_SDA_9": 24, "WIRE0_SDA_9": 25,
"WIRE0_SDA_10": 25,
"SERIAL0_RX": 3, "SERIAL0_RX": 3,
"SERIAL0_TX": 2, "SERIAL0_TX": 2,
"SERIAL1_RX": 24, "SERIAL1_RX": 24,
@@ -221,32 +219,32 @@ LN882X_BOARD_PINS = {
"A1": 4, "A1": 4,
}, },
"wl2s": { "wl2s": {
"WIRE0_SCL_0": 7, "WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 12, "WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 3, "WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 10, "WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 2, "WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 0, "WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 19, "WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 11, "WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 9, "WIRE0_SCL_8": 11,
"WIRE0_SCL_9": 24, "WIRE0_SCL_9": 12,
"WIRE0_SCL_10": 25, "WIRE0_SCL_10": 19,
"WIRE0_SCL_11": 5, "WIRE0_SCL_11": 24,
"WIRE0_SCL_12": 1, "WIRE0_SCL_12": 25,
"WIRE0_SDA_0": 7, "WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 12, "WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 3, "WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 10, "WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 2, "WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 0, "WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 19, "WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 11, "WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 9, "WIRE0_SDA_8": 11,
"WIRE0_SDA_9": 24, "WIRE0_SDA_9": 12,
"WIRE0_SDA_10": 25, "WIRE0_SDA_10": 19,
"WIRE0_SDA_11": 5, "WIRE0_SDA_11": 24,
"WIRE0_SDA_12": 1, "WIRE0_SDA_12": 25,
"SERIAL0_RX": 3, "SERIAL0_RX": 3,
"SERIAL0_TX": 2, "SERIAL0_TX": 2,
"SERIAL1_RX": 24, "SERIAL1_RX": 24,
@@ -301,24 +299,24 @@ LN882X_BOARD_PINS = {
"A2": 1, "A2": 1,
}, },
"ln-02": { "ln-02": {
"WIRE0_SCL_0": 11, "WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 19, "WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 3, "WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 24, "WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 2, "WIRE0_SCL_4": 9,
"WIRE0_SCL_5": 25, "WIRE0_SCL_5": 11,
"WIRE0_SCL_6": 1, "WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 0, "WIRE0_SCL_7": 24,
"WIRE0_SCL_8": 9, "WIRE0_SCL_8": 25,
"WIRE0_SDA_0": 11, "WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 19, "WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 3, "WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 24, "WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 2, "WIRE0_SDA_4": 9,
"WIRE0_SDA_5": 25, "WIRE0_SDA_5": 11,
"WIRE0_SDA_6": 1, "WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 0, "WIRE0_SDA_7": 24,
"WIRE0_SDA_8": 9, "WIRE0_SDA_8": 25,
"SERIAL0_RX": 3, "SERIAL0_RX": 3,
"SERIAL0_TX": 2, "SERIAL0_TX": 2,
"SERIAL1_RX": 24, "SERIAL1_RX": 24,

View File

@@ -45,9 +45,28 @@ class MDNSComponent : public Component {
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_ARDUINO) // Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040).
void loop() override; //
#endif // On these platforms, MDNS.update() calls _process(true) which only manages timer-driven
// state machines (probe/announce timeouts and service query cache TTLs). Incoming mDNS
// packets are handled independently via the lwIP onRx UDP callback and are NOT affected
// by how often update() is called.
//
// The shortest internal timer is the 250ms probe interval (RFC 6762 Section 8.1).
// Announcement intervals are 1000ms and cache TTL checks are on the order of seconds
// to minutes. A 50ms polling interval provides sufficient resolution for all timers
// while completely removing mDNS from the per-iteration loop list.
//
// In steady state (after the ~8 second boot probe/announce phase completes), update()
// checks timers that are set to never expire, making every call pure overhead.
//
// Tasmota uses a 50ms main loop cycle with mDNS working correctly, confirming this
// interval is safe in production.
//
// By using set_interval() instead of overriding loop(), the component is excluded from
// the main loop list via has_overridden_loop(), eliminating all per-iteration overhead
// including virtual dispatch.
static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50;
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
#ifdef USE_MDNS_EXTRA_SERVICES #ifdef USE_MDNS_EXTRA_SERVICES

View File

@@ -36,9 +36,14 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
} }
} }
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp8266); } void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_esp8266);
void MDNSComponent::loop() { MDNS.update(); } // Schedule MDNS.update() via set_interval() instead of overriding loop().
// This removes the component from the per-iteration loop list entirely,
// eliminating virtual dispatch overhead on every main loop cycle.
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
}
void MDNSComponent::on_shutdown() { void MDNSComponent::on_shutdown() {
MDNS.close(); MDNS.close();

View File

@@ -35,9 +35,14 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
} }
} }
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_rp2040); } void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_rp2040);
void MDNSComponent::loop() { MDNS.update(); } // Schedule MDNS.update() via set_interval() instead of overriding loop().
// This removes the component from the per-iteration loop list entirely,
// eliminating virtual dispatch overhead on every main loop cycle.
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
}
void MDNSComponent::on_shutdown() { void MDNSComponent::on_shutdown() {
MDNS.close(); MDNS.close();

View File

@@ -170,10 +170,8 @@ void MQTTClientComponent::send_device_info_() {
void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
(void) tag; (void) tag;
if (level <= this->log_level_ && this->is_connected()) { if (level <= this->log_level_ && this->is_connected()) {
this->publish({.topic = this->log_message_.topic, this->publish(this->log_message_.topic.c_str(), message, message_len, this->log_message_.qos,
.payload = std::string(message, message_len), this->log_message_.retain);
.qos = this->log_message_.qos,
.retain = this->log_message_.retain});
} }
} }
#endif #endif

View File

@@ -300,9 +300,11 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device
bool MQTTClimateComponent::publish_state_() { bool MQTTClimateComponent::publish_state_() {
auto traits = this->device_->get_traits(); auto traits = this->device_->get_traits();
// Reusable stack buffer for topic construction (avoids heap allocation per publish)
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
// mode // mode
bool success = true; bool success = true;
if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode))) if (!this->publish(this->get_mode_state_topic_to(topic_buf), climate_mode_to_mqtt_str(this->device_->mode)))
success = false; success = false;
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
@@ -311,68 +313,70 @@ bool MQTTClimateComponent::publish_state_() {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) && if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
!std::isnan(this->device_->current_temperature)) { !std::isnan(this->device_->current_temperature)) {
len = value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy); len = value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy);
if (!this->publish(this->get_current_temperature_state_topic(), payload, len)) if (!this->publish(this->get_current_temperature_state_topic_to(topic_buf), payload, len))
success = false; success = false;
} }
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy); len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy);
if (!this->publish(this->get_target_temperature_low_state_topic(), payload, len)) if (!this->publish(this->get_target_temperature_low_state_topic_to(topic_buf), payload, len))
success = false; success = false;
len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy); len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy);
if (!this->publish(this->get_target_temperature_high_state_topic(), payload, len)) if (!this->publish(this->get_target_temperature_high_state_topic_to(topic_buf), payload, len))
success = false; success = false;
} else { } else {
len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy); len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy);
if (!this->publish(this->get_target_temperature_state_topic(), payload, len)) if (!this->publish(this->get_target_temperature_state_topic_to(topic_buf), payload, len))
success = false; success = false;
} }
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) && if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
!std::isnan(this->device_->current_humidity)) { !std::isnan(this->device_->current_humidity)) {
len = value_accuracy_to_buf(payload, this->device_->current_humidity, 0); len = value_accuracy_to_buf(payload, this->device_->current_humidity, 0);
if (!this->publish(this->get_current_humidity_state_topic(), payload, len)) if (!this->publish(this->get_current_humidity_state_topic_to(topic_buf), payload, len))
success = false; success = false;
} }
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) && if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
!std::isnan(this->device_->target_humidity)) { !std::isnan(this->device_->target_humidity)) {
len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0); len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0);
if (!this->publish(this->get_target_humidity_state_topic(), payload, len)) if (!this->publish(this->get_target_humidity_state_topic_to(topic_buf), payload, len))
success = false; success = false;
} }
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
if (this->device_->has_custom_preset()) { if (this->device_->has_custom_preset()) {
if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset())) if (!this->publish(this->get_preset_state_topic_to(topic_buf), this->device_->get_custom_preset().c_str()))
success = false; success = false;
} else if (this->device_->preset.has_value()) { } else if (this->device_->preset.has_value()) {
if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value()))) if (!this->publish(this->get_preset_state_topic_to(topic_buf),
climate_preset_to_mqtt_str(this->device_->preset.value())))
success = false; success = false;
} else if (!this->publish(this->get_preset_state_topic(), "")) { } else if (!this->publish(this->get_preset_state_topic_to(topic_buf), "")) {
success = false; success = false;
} }
} }
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action))) if (!this->publish(this->get_action_state_topic_to(topic_buf), climate_action_to_mqtt_str(this->device_->action)))
success = false; success = false;
} }
if (traits.get_supports_fan_modes()) { if (traits.get_supports_fan_modes()) {
if (this->device_->has_custom_fan_mode()) { if (this->device_->has_custom_fan_mode()) {
if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode())) if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), this->device_->get_custom_fan_mode().c_str()))
success = false; success = false;
} else if (this->device_->fan_mode.has_value()) { } else if (this->device_->fan_mode.has_value()) {
if (!this->publish(this->get_fan_mode_state_topic(), if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf),
climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value()))) climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value())))
success = false; success = false;
} else if (!this->publish(this->get_fan_mode_state_topic(), "")) { } else if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), "")) {
success = false; success = false;
} }
} }
if (traits.get_supports_swing_modes()) { if (traits.get_supports_swing_modes()) {
if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode))) if (!this->publish(this->get_swing_mode_state_topic_to(topic_buf),
climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
success = false; success = false;
} }

View File

@@ -59,6 +59,11 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
\ \
public: \ public: \
void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \ void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \
StringRef get_##name##_##type##_topic_to(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const { \
if (!this->custom_##name##_##type##_topic_.empty()) \
return StringRef(this->custom_##name##_##type##_topic_.data(), this->custom_##name##_##type##_topic_.size()); \
return this->get_default_topic_for_to_(buf, #name "/" #type, sizeof(#name "/" #type) - 1); \
} \
std::string get_##name##_##type##_topic() const { \ std::string get_##name##_##type##_topic() const { \
if (this->custom_##name##_##type##_topic_.empty()) \ if (this->custom_##name##_##type##_topic_.empty()) \
return this->get_default_topic_for_(#name "/" #type); \ return this->get_default_topic_for_(#name "/" #type); \

View File

@@ -112,19 +112,19 @@ bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
bool MQTTCoverComponent::publish_state() { bool MQTTCoverComponent::publish_state() {
auto traits = this->cover_->get_traits(); auto traits = this->cover_->get_traits();
bool success = true; bool success = true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (traits.get_supports_position()) { if (traits.get_supports_position()) {
char pos[VALUE_ACCURACY_MAX_LEN]; char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0); size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);
if (!this->publish(this->get_position_state_topic(), pos, len)) if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len))
success = false; success = false;
} }
if (traits.get_supports_tilt()) { if (traits.get_supports_tilt()) {
char pos[VALUE_ACCURACY_MAX_LEN]; char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0); size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0);
if (!this->publish(this->get_tilt_state_topic(), pos, len)) if (!this->publish(this->get_tilt_state_topic_to(topic_buf), pos, len))
success = false; success = false;
} }
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf), if (!this->publish(this->get_state_topic_to_(topic_buf),
cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position, cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
traits.get_supports_position()))) traits.get_supports_position())))

View File

@@ -173,19 +173,20 @@ bool MQTTFanComponent::publish_state() {
this->publish(this->get_state_topic_to_(topic_buf), state_s); this->publish(this->get_state_topic_to_(topic_buf), state_s);
bool failed = false; bool failed = false;
if (this->state_->get_traits().supports_direction()) { if (this->state_->get_traits().supports_direction()) {
bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction)); bool success = this->publish(this->get_direction_state_topic_to(topic_buf),
fan_direction_to_mqtt_str(this->state_->direction));
failed = failed || !success; failed = failed || !success;
} }
if (this->state_->get_traits().supports_oscillation()) { if (this->state_->get_traits().supports_oscillation()) {
bool success = bool success = this->publish(this->get_oscillation_state_topic_to(topic_buf),
this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating)); fan_oscillation_to_mqtt_str(this->state_->oscillating));
failed = failed || !success; failed = failed || !success;
} }
auto traits = this->state_->get_traits(); auto traits = this->state_->get_traits();
if (traits.supports_speed()) { if (traits.supports_speed()) {
char buf[12]; char buf[12];
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed); size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
bool success = this->publish(this->get_speed_level_state_topic(), buf, len); bool success = this->publish(this->get_speed_level_state_topic_to(topic_buf), buf, len);
failed = failed || !success; failed = failed || !success;
} }
return !failed; return !failed;

View File

@@ -87,13 +87,13 @@ bool MQTTValveComponent::send_initial_state() { return this->publish_state(); }
bool MQTTValveComponent::publish_state() { bool MQTTValveComponent::publish_state() {
auto traits = this->valve_->get_traits(); auto traits = this->valve_->get_traits();
bool success = true; bool success = true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (traits.get_supports_position()) { if (traits.get_supports_position()) {
char pos[VALUE_ACCURACY_MAX_LEN]; char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0); size_t len = value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0);
if (!this->publish(this->get_position_state_topic(), pos, len)) if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len))
success = false; success = false;
} }
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf), if (!this->publish(this->get_state_topic_to_(topic_buf),
valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position, valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position,
traits.get_supports_position()))) traits.get_supports_position())))

View File

@@ -71,6 +71,10 @@ RTL87XX_BOARDS = {
"name": "WR3L Wi-Fi Module", "name": "WR3L Wi-Fi Module",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
}, },
"wbru": {
"name": "WBRU Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"wr2le": { "wr2le": {
"name": "WR2LE Wi-Fi Module", "name": "WR2LE Wi-Fi Module",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
@@ -83,6 +87,14 @@ RTL87XX_BOARDS = {
"name": "T103_V1.0", "name": "T103_V1.0",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
}, },
"cr3l": {
"name": "CR3L Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"generic-rtl8720cm-4mb-1712k": {
"name": "Generic - RTL8720CM (4M/1712k)",
"family": FAMILY_RTL8720C,
},
"generic-rtl8720cf-2mb-896k": { "generic-rtl8720cf-2mb-896k": {
"name": "Generic - RTL8720CF (2M/896k)", "name": "Generic - RTL8720CF (2M/896k)",
"family": FAMILY_RTL8720C, "family": FAMILY_RTL8720C,
@@ -103,6 +115,10 @@ RTL87XX_BOARDS = {
"name": "WR2L Wi-Fi Module", "name": "WR2L Wi-Fi Module",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
}, },
"wbr1": {
"name": "WBR1 Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"wr1": { "wr1": {
"name": "WR1 Wi-Fi Module", "name": "WR1 Wi-Fi Module",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
@@ -119,10 +135,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 30, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 19, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
"WIRE1_SDA": 23, "WIRE1_SDA": 23,
"SERIAL0_CTS": 19, "SERIAL0_CTS": 19,
@@ -230,10 +246,10 @@ RTL87XX_BOARD_PINS = {
"A1": 41, "A1": 41,
}, },
"wbr3": { "wbr3": {
"WIRE0_SCL_0": 11, "WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 2, "WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 19, "WIRE0_SCL_2": 15,
"WIRE0_SCL_3": 15, "WIRE0_SCL_3": 19,
"WIRE0_SDA_0": 3, "WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12, "WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16, "WIRE0_SDA_2": 16,
@@ -242,10 +258,10 @@ RTL87XX_BOARD_PINS = {
"SERIAL0_TX_0": 11, "SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14, "SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4, "SERIAL1_CTS": 4,
"SERIAL1_RX_0": 2, "SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 0, "SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 3, "SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 1, "SERIAL1_TX_1": 3,
"SERIAL2_CTS": 19, "SERIAL2_CTS": 19,
"SERIAL2_RX": 15, "SERIAL2_RX": 15,
"SERIAL2_TX": 16, "SERIAL2_TX": 16,
@@ -296,6 +312,12 @@ RTL87XX_BOARD_PINS = {
}, },
"generic-rtl8710bn-2mb-468k": { "generic-rtl8710bn-2mb-468k": {
"SPI0_CS": 19, "SPI0_CS": 19,
"SPI0_FCS": 6,
"SPI0_FD0": 9,
"SPI0_FD1": 7,
"SPI0_FD2": 8,
"SPI0_FD3": 11,
"SPI0_FSCK": 10,
"SPI0_MISO": 22, "SPI0_MISO": 22,
"SPI0_MOSI": 23, "SPI0_MOSI": 23,
"SPI0_SCK": 18, "SPI0_SCK": 18,
@@ -396,10 +418,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 30, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 19, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
"WIRE1_SDA": 23, "WIRE1_SDA": 23,
"SERIAL0_CTS": 19, "SERIAL0_CTS": 19,
@@ -463,10 +485,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 30, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 19, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
"WIRE1_SDA": 23, "WIRE1_SDA": 23,
"SERIAL0_CTS": 19, "SERIAL0_CTS": 19,
@@ -714,6 +736,12 @@ RTL87XX_BOARD_PINS = {
}, },
"generic-rtl8710bn-2mb-788k": { "generic-rtl8710bn-2mb-788k": {
"SPI0_CS": 19, "SPI0_CS": 19,
"SPI0_FCS": 6,
"SPI0_FD0": 9,
"SPI0_FD1": 7,
"SPI0_FD2": 8,
"SPI0_FD3": 11,
"SPI0_FSCK": 10,
"SPI0_MISO": 22, "SPI0_MISO": 22,
"SPI0_MOSI": 23, "SPI0_MOSI": 23,
"SPI0_SCK": 18, "SPI0_SCK": 18,
@@ -807,6 +835,12 @@ RTL87XX_BOARD_PINS = {
}, },
"generic-rtl8710bx-4mb-980k": { "generic-rtl8710bx-4mb-980k": {
"SPI0_CS": 19, "SPI0_CS": 19,
"SPI0_FCS": 6,
"SPI0_FD0": 9,
"SPI0_FD1": 7,
"SPI0_FD2": 8,
"SPI0_FD3": 11,
"SPI0_FSCK": 10,
"SPI0_MISO": 22, "SPI0_MISO": 22,
"SPI0_MOSI": 23, "SPI0_MOSI": 23,
"SPI0_SCK": 18, "SPI0_SCK": 18,
@@ -957,8 +991,8 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 19, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
@@ -1088,6 +1122,99 @@ RTL87XX_BOARD_PINS = {
"A0": 19, "A0": 19,
"A1": 41, "A1": 41,
}, },
"wbru": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 7,
"SPI0_CS_2": 15,
"SPI0_MISO_0": 10,
"SPI0_MISO_1": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 9,
"SPI0_MOSI_2": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 8,
"SPI0_SCK_2": 16,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SCL_3": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"WIRE0_SDA_3": 20,
"SERIAL0_CTS": 10,
"SERIAL0_RTS": 9,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX": 3,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CS0": 7,
"CTS0": 10,
"CTS1": 4,
"CTS2": 19,
"MOSI0": 19,
"PA00": 0,
"PA0": 0,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PA20": 20,
"PWM0": 0,
"PWM1": 12,
"PWM5": 17,
"PWM6": 18,
"RTS0": 9,
"RTS2": 20,
"RX2": 15,
"SCK0": 16,
"TX1": 3,
"TX2": 16,
"D0": 8,
"D1": 9,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 15,
"D6": 16,
"D7": 11,
"D8": 12,
"D9": 17,
"D10": 18,
"D11": 19,
"D12": 14,
"D13": 13,
"D14": 20,
"D15": 0,
"D16": 10,
"D17": 7,
},
"wr2le": { "wr2le": {
"MISO0": 22, "MISO0": 22,
"MISO1": 22, "MISO1": 22,
@@ -1116,21 +1243,21 @@ RTL87XX_BOARD_PINS = {
"SPI0_MISO": 20, "SPI0_MISO": 20,
"SPI0_MOSI_0": 4, "SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 19, "SPI0_MOSI_1": 19,
"SPI0_SCK_0": 16, "SPI0_SCK_0": 3,
"SPI0_SCK_1": 3, "SPI0_SCK_1": 16,
"WIRE0_SCL_0": 2, "WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 15, "WIRE0_SCL_1": 15,
"WIRE0_SCL_2": 19, "WIRE0_SCL_2": 19,
"WIRE0_SDA_0": 20, "WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 16, "WIRE0_SDA_1": 16,
"WIRE0_SDA_2": 3, "WIRE0_SDA_2": 20,
"SERIAL0_RX": 13, "SERIAL0_RX": 13,
"SERIAL0_TX": 14, "SERIAL0_TX": 14,
"SERIAL1_CTS": 4, "SERIAL1_CTS": 4,
"SERIAL1_RX_0": 2, "SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 0, "SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 3, "SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 1, "SERIAL1_TX_1": 3,
"SERIAL2_CTS": 19, "SERIAL2_CTS": 19,
"SERIAL2_RTS": 20, "SERIAL2_RTS": 20,
"SERIAL2_RX": 15, "SERIAL2_RX": 15,
@@ -1251,6 +1378,168 @@ RTL87XX_BOARD_PINS = {
"A0": 19, "A0": 19,
"A1": 41, "A1": 41,
}, },
"cr3l": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 15,
"SPI0_MISO": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 16,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 15,
"WIRE0_SCL_2": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 16,
"WIRE0_SDA_2": 20,
"SERIAL0_RX": 13,
"SERIAL0_TX": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX": 2,
"SERIAL1_TX": 3,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CTS1": 4,
"CTS2": 19,
"MISO0": 20,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PA20": 20,
"PWM0": 20,
"PWM5": 17,
"PWM6": 18,
"RTS2": 20,
"RX0": 13,
"RX1": 2,
"RX2": 15,
"SCL0": 19,
"SDA0": 16,
"TX0": 14,
"TX1": 3,
"TX2": 16,
"D0": 20,
"D1": 2,
"D2": 3,
"D3": 4,
"D4": 15,
"D5": 16,
"D6": 17,
"D7": 18,
"D8": 19,
"D9": 13,
"D10": 14,
},
"generic-rtl8720cm-4mb-1712k": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 7,
"SPI0_CS_2": 15,
"SPI0_MISO_0": 10,
"SPI0_MISO_1": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 9,
"SPI0_MOSI_2": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 8,
"SPI0_SCK_2": 16,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SCL_3": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"WIRE0_SDA_3": 20,
"SERIAL0_CTS": 10,
"SERIAL0_RTS": 9,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 3,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CS0": 15,
"CTS0": 10,
"CTS1": 4,
"CTS2": 19,
"MOSI0": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PA20": 20,
"PA23": 23,
"PWM0": 20,
"PWM5": 17,
"PWM6": 18,
"PWM7": 23,
"RTS0": 9,
"RTS2": 20,
"RX2": 15,
"SCK0": 16,
"TX2": 16,
"D0": 0,
"D1": 1,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 7,
"D6": 8,
"D7": 9,
"D8": 10,
"D9": 11,
"D10": 12,
"D11": 13,
"D12": 14,
"D13": 15,
"D14": 16,
"D15": 17,
"D16": 18,
"D17": 19,
"D18": 20,
"D19": 23,
},
"generic-rtl8720cf-2mb-896k": { "generic-rtl8720cf-2mb-896k": {
"SPI0_CS_0": 2, "SPI0_CS_0": 2,
"SPI0_CS_1": 7, "SPI0_CS_1": 7,
@@ -1456,8 +1745,8 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 19, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
@@ -1585,6 +1874,65 @@ RTL87XX_BOARD_PINS = {
"D4": 12, "D4": 12,
"A0": 19, "A0": 19,
}, },
"wbr1": {
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 3,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CTS1": 4,
"MOSI0": 4,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PWM5": 17,
"PWM6": 18,
"PWM7": 13,
"RX2": 15,
"SCL0": 15,
"SDA0": 12,
"TX2": 16,
"D0": 14,
"D1": 13,
"D2": 2,
"D3": 3,
"D4": 16,
"D5": 4,
"D6": 11,
"D7": 15,
"D8": 12,
"D9": 17,
"D10": 18,
"D11": 0,
"D12": 1,
},
"wr1": { "wr1": {
"SPI0_CS": 19, "SPI0_CS": 19,
"SPI0_MISO": 22, "SPI0_MISO": 22,
@@ -1594,10 +1942,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 30, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 19, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
"WIRE1_SDA": 23, "WIRE1_SDA": 23,
"SERIAL0_CTS": 19, "SERIAL0_CTS": 19,

View File

@@ -2,29 +2,74 @@
#include <cmath> #include <cmath>
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome { namespace esphome::rtttl {
namespace rtttl {
static const char *const TAG = "rtttl"; static const char *const TAG = "rtttl";
static const uint32_t DOUBLE_NOTE_GAP_MS = 10;
// These values can also be found as constants in the Tone library (Tone.h) // These values can also be found as constants in the Tone library (Tone.h)
static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047,
1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217,
2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951};
static const uint16_t I2S_SPEED = 1000; #if defined(USE_OUTPUT) || defined(USE_SPEAKER)
static const uint32_t DOUBLE_NOTE_GAP_MS = 10;
#endif // USE_OUTPUT || USE_SPEAKER
#undef HALF_PI #ifdef USE_SPEAKER
static const double HALF_PI = 1.5707963267948966192313216916398; static const size_t SAMPLE_BUFFER_SIZE = 2048;
struct SpeakerSample {
int8_t left{0};
int8_t right{0};
};
inline double deg2rad(double degrees) { inline double deg2rad(double degrees) {
static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0; static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0;
return degrees * PI_ON_180; return degrees * PI_ON_180;
} }
#endif // USE_SPEAKER
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
// RTTTL state strings indexed by State enum (0-4): STOPPED, INIT, STARTING, RUNNING, STOPPING, plus UNKNOWN fallback
PROGMEM_STRING_TABLE(RtttlStateStrings, "State::STOPPED", "State::INIT", "State::STARTING", "State::RUNNING",
"State::STOPPING", "UNKNOWN");
static const LogString *state_to_string(State state) {
return RtttlStateStrings::get_log_str(static_cast<uint8_t>(state), RtttlStateStrings::LAST_INDEX);
}
#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
static uint8_t note_index_from_char(char note) {
switch (note) {
case 'c':
return 1;
// 'c#': 2
case 'd':
return 3;
// 'd#': 4
case 'e':
return 5;
case 'f':
return 6;
// 'f#': 7
case 'g':
return 8;
// 'g#': 9
case 'a':
return 10;
// 'a#': 11
// Support both 'b' (English notation for B natural) and 'h' (German notation for B natural)
case 'b':
case 'h':
return 12;
case 'p':
default:
return 0;
}
}
void Rtttl::dump_config() { void Rtttl::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
@@ -33,161 +78,34 @@ void Rtttl::dump_config() {
this->gain_); this->gain_);
} }
void Rtttl::play(std::string rtttl) {
if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) {
size_t pos = this->rtttl_.find(':');
size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length();
ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str());
return;
}
this->rtttl_ = std::move(rtttl);
this->default_duration_ = 4;
this->default_octave_ = 6;
this->note_duration_ = 0;
int bpm = 63;
uint8_t num;
// Get name
this->position_ = this->rtttl_.find(':');
// it's somewhat documented to be up to 10 characters but let's be a bit flexible here
if (this->position_ == std::string::npos || this->position_ > 15) {
ESP_LOGE(TAG, "Unable to determine name; missing ':'");
return;
}
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
// get default duration
this->position_ = this->rtttl_.find("d=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'd='");
return;
}
this->position_ += 2;
num = this->get_integer_();
if (num > 0)
this->default_duration_ = num;
// get default octave
this->position_ = this->rtttl_.find("o=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'o=");
return;
}
this->position_ += 2;
num = get_integer_();
if (num >= 3 && num <= 7)
this->default_octave_ = num;
// get BPM
this->position_ = this->rtttl_.find("b=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing b=");
return;
}
this->position_ += 2;
num = get_integer_();
if (num != 0)
bpm = num;
this->position_ = this->rtttl_.find(':', this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing second ':'");
return;
}
this->position_++;
// BPM usually expresses the number of quarter notes per minute
this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds)
this->output_freq_ = 0;
this->last_note_ = millis();
this->note_duration_ = 1;
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
this->set_state_(State::STATE_INIT);
this->samples_sent_ = 0;
this->samples_count_ = 0;
}
#endif
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
this->set_state_(State::STATE_RUNNING);
}
#endif
}
void Rtttl::stop() {
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
this->output_->set_level(0.0);
this->set_state_(STATE_STOPPED);
}
#endif
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
if (this->speaker_->is_running()) {
this->speaker_->stop();
}
this->set_state_(STATE_STOPPING);
}
#endif
this->position_ = this->rtttl_.length();
this->note_duration_ = 0;
}
void Rtttl::finish_() {
ESP_LOGV(TAG, "Rtttl::finish_()");
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
this->output_->set_level(0.0);
this->set_state_(State::STATE_STOPPED);
}
#endif
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
SpeakerSample sample[2];
sample[0].left = 0;
sample[0].right = 0;
sample[1].left = 0;
sample[1].right = 0;
this->speaker_->play((uint8_t *) (&sample), 8);
this->speaker_->finish();
this->set_state_(State::STATE_STOPPING);
}
#endif
// Ensure no more notes are played in case finish_() is called for an error.
this->position_ = this->rtttl_.length();
this->note_duration_ = 0;
}
void Rtttl::loop() { void Rtttl::loop() {
if (this->state_ == State::STATE_STOPPED) { if (this->state_ == State::STOPPED) {
this->disable_loop(); this->disable_loop();
return; return;
} }
#ifdef USE_OUTPUT
if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) {
return;
}
#endif // USE_OUTPUT
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
if (this->speaker_ != nullptr) { if (this->speaker_ != nullptr) {
if (this->state_ == State::STATE_STOPPING) { if (this->state_ == State::STOPPING) {
if (this->speaker_->is_stopped()) { if (this->speaker_->is_stopped()) {
this->set_state_(State::STATE_STOPPED); this->set_state_(State::STOPPED);
} else { } else {
return; return;
} }
} else if (this->state_ == State::STATE_INIT) { } else if (this->state_ == State::INIT) {
if (this->speaker_->is_stopped()) { if (this->speaker_->is_stopped()) {
this->speaker_->start(); this->speaker_->start();
this->set_state_(State::STATE_STARTING); this->set_state_(State::STARTING);
} }
} else if (this->state_ == State::STATE_STARTING) { } else if (this->state_ == State::STARTING) {
if (this->speaker_->is_running()) { if (this->speaker_->is_running()) {
this->set_state_(State::STATE_RUNNING); this->set_state_(State::RUNNING);
} }
} }
if (!this->speaker_->is_running()) { if (!this->speaker_->is_running()) {
@@ -229,19 +147,17 @@ void Rtttl::loop() {
} }
} }
} }
#endif #endif // USE_SPEAKER
#ifdef USE_OUTPUT
if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_)
return;
#endif
if (this->position_ >= this->rtttl_.length()) { if (this->position_ >= this->rtttl_.length()) {
this->finish_(); this->finish_();
return; return;
} }
// align to note: most rtttl's out there does not add and space after the ',' separator but just in case... // align to note: most rtttl's out there does not add and space after the ',' separator but just in case...
while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') {
this->position_++; this->position_++;
}
// first, get note duration, if available // first, get note duration, if available
uint8_t num = this->get_integer_(); uint8_t num = this->get_integer_();
@@ -253,35 +169,8 @@ void Rtttl::loop() {
this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after
} }
uint8_t note; uint8_t note = note_index_from_char(this->rtttl_[this->position_]);
switch (this->rtttl_[this->position_]) {
case 'c':
note = 1;
break;
case 'd':
note = 3;
break;
case 'e':
note = 5;
break;
case 'f':
note = 6;
break;
case 'g':
note = 8;
break;
case 'a':
note = 10;
break;
case 'h':
case 'b':
note = 12;
break;
case 'p':
default:
note = 0;
}
this->position_++; this->position_++;
// now, get optional '#' sharp // now, get optional '#' sharp
@@ -291,7 +180,7 @@ void Rtttl::loop() {
} }
// now, get scale // now, get scale
uint8_t scale = get_integer_(); uint8_t scale = this->get_integer_();
if (scale == 0) { if (scale == 0) {
scale = this->default_octave_; scale = this->default_octave_;
} }
@@ -344,7 +233,8 @@ void Rtttl::loop() {
this->output_->set_level(0.0); this->output_->set_level(0.0);
} }
} }
#endif #endif // USE_OUTPUT
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
if (this->speaker_ != nullptr) { if (this->speaker_ != nullptr) {
this->samples_sent_ = 0; this->samples_sent_ = 0;
@@ -369,29 +259,152 @@ void Rtttl::loop() {
} }
// Convert from frequency in Hz to high and low samples in fixed point // Convert from frequency in Hz to high and low samples in fixed point
} }
#endif #endif // USE_SPEAKER
this->last_note_ = millis(); this->last_note_ = millis();
} }
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE void Rtttl::play(std::string rtttl) {
static const LogString *state_to_string(State state) { if (this->state_ != State::STOPPED && this->state_ != State::STOPPING) {
switch (state) { size_t pos = this->rtttl_.find(':');
case STATE_STOPPED: size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length();
return LOG_STR("STATE_STOPPED"); ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str());
case STATE_STARTING: return;
return LOG_STR("STATE_STARTING");
case STATE_RUNNING:
return LOG_STR("STATE_RUNNING");
case STATE_STOPPING:
return LOG_STR("STATE_STOPPING");
case STATE_INIT:
return LOG_STR("STATE_INIT");
default:
return LOG_STR("UNKNOWN");
} }
};
#endif this->rtttl_ = std::move(rtttl);
this->default_duration_ = 4;
this->default_octave_ = 6;
this->note_duration_ = 0;
int bpm = 63;
uint8_t num;
// Get name
this->position_ = this->rtttl_.find(':');
// it's somewhat documented to be up to 10 characters but let's be a bit flexible here
if (this->position_ == std::string::npos || this->position_ > 15) {
ESP_LOGE(TAG, "Unable to determine name; missing ':'");
return;
}
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
// get default duration
this->position_ = this->rtttl_.find("d=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'd='");
return;
}
this->position_ += 2;
num = this->get_integer_();
if (num > 0) {
this->default_duration_ = num;
}
// get default octave
this->position_ = this->rtttl_.find("o=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'o=");
return;
}
this->position_ += 2;
num = this->get_integer_();
if (num >= 3 && num <= 7) {
this->default_octave_ = num;
}
// get BPM
this->position_ = this->rtttl_.find("b=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing b=");
return;
}
this->position_ += 2;
num = this->get_integer_();
if (num != 0) {
bpm = num;
}
this->position_ = this->rtttl_.find(':', this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing second ':'");
return;
}
this->position_++;
// BPM usually expresses the number of quarter notes per minute
this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds)
this->output_freq_ = 0;
this->last_note_ = millis();
this->note_duration_ = 1;
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
this->set_state_(State::RUNNING);
}
#endif // USE_OUTPUT
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
this->set_state_(State::INIT);
this->samples_sent_ = 0;
this->samples_count_ = 0;
}
#endif // USE_SPEAKER
}
void Rtttl::stop() {
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
this->output_->set_level(0.0);
this->set_state_(State::STOPPED);
}
#endif // USE_OUTPUT
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
if (this->speaker_->is_running()) {
this->speaker_->stop();
}
this->set_state_(State::STOPPING);
}
#endif // USE_SPEAKER
this->position_ = this->rtttl_.length();
this->note_duration_ = 0;
}
void Rtttl::finish_() {
ESP_LOGV(TAG, "Rtttl::finish_()");
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
this->output_->set_level(0.0);
this->set_state_(State::STOPPED);
}
#endif // USE_OUTPUT
#ifdef USE_SPEAKER
if (this->speaker_ != nullptr) {
SpeakerSample sample[2];
sample[0].left = 0;
sample[0].right = 0;
sample[1].left = 0;
sample[1].right = 0;
this->speaker_->play((uint8_t *) (&sample), 8);
this->speaker_->finish();
this->set_state_(State::STOPPING);
}
#endif // USE_SPEAKER
// Ensure no more notes are played in case finish_() is called for an error.
this->position_ = this->rtttl_.length();
this->note_duration_ = 0;
}
void Rtttl::set_state_(State state) { void Rtttl::set_state_(State state) {
State old_state = this->state_; State old_state = this->state_;
@@ -399,15 +412,14 @@ void Rtttl::set_state_(State state) {
ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)),
LOG_STR_ARG(state_to_string(state))); LOG_STR_ARG(state_to_string(state)));
// Clear loop_done when transitioning from STOPPED to any other state // Clear loop_done when transitioning from `State::STOPPED` to any other state
if (state == State::STATE_STOPPED) { if (state == State::STOPPED) {
this->disable_loop(); this->disable_loop();
this->on_finished_playback_callback_.call(); this->on_finished_playback_callback_.call();
ESP_LOGD(TAG, "Playback finished"); ESP_LOGD(TAG, "Playback finished");
} else if (old_state == State::STATE_STOPPED) { } else if (old_state == State::STOPPED) {
this->enable_loop(); this->enable_loop();
} }
} }
} // namespace rtttl } // namespace esphome::rtttl
} // namespace esphome

View File

@@ -5,48 +5,41 @@
#ifdef USE_OUTPUT #ifdef USE_OUTPUT
#include "esphome/components/output/float_output.h" #include "esphome/components/output/float_output.h"
#endif #endif // USE_OUTPUT
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
#include "esphome/components/speaker/speaker.h" #include "esphome/components/speaker/speaker.h"
#endif #endif // USE_SPEAKER
namespace esphome { namespace esphome::rtttl {
namespace rtttl {
enum State : uint8_t { enum class State : uint8_t {
STATE_STOPPED = 0, STOPPED = 0,
STATE_INIT, INIT,
STATE_STARTING, STARTING,
STATE_RUNNING, RUNNING,
STATE_STOPPING, STOPPING,
}; };
#ifdef USE_SPEAKER
static const size_t SAMPLE_BUFFER_SIZE = 2048;
struct SpeakerSample {
int8_t left{0};
int8_t right{0};
};
#endif
class Rtttl : public Component { class Rtttl : public Component {
public: public:
#ifdef USE_OUTPUT #ifdef USE_OUTPUT
void set_output(output::FloatOutput *output) { this->output_ = output; } void set_output(output::FloatOutput *output) { this->output_ = output; }
#endif #endif // USE_OUTPUT
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; }
#endif #endif // USE_SPEAKER
float get_gain() { return gain_; }
void set_gain(float gain) { this->gain_ = clamp(gain, 0.0f, 1.0f); } void dump_config() override;
void loop() override;
void play(std::string rtttl); void play(std::string rtttl);
void stop(); void stop();
void dump_config() override;
bool is_playing() { return this->state_ != State::STATE_STOPPED; } float get_gain() { return this->gain_; }
void loop() override; void set_gain(float gain) { this->gain_ = clamp(gain, 0.0f, 1.0f); }
bool is_playing() { return this->state_ != State::STOPPED; }
void add_on_finished_playback_callback(std::function<void()> callback) { void add_on_finished_playback_callback(std::function<void()> callback) {
this->on_finished_playback_callback_.add(std::move(callback)); this->on_finished_playback_callback_.add(std::move(callback));
@@ -90,12 +83,12 @@ class Rtttl : public Component {
/// The gain of the output. /// The gain of the output.
float gain_{0.6f}; float gain_{0.6f};
/// The current state of the RTTTL player. /// The current state of the RTTTL player.
State state_{State::STATE_STOPPED}; State state_{State::STOPPED};
#ifdef USE_OUTPUT #ifdef USE_OUTPUT
/// The output to write the sound to. /// The output to write the sound to.
output::FloatOutput *output_; output::FloatOutput *output_;
#endif #endif // USE_OUTPUT
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
/// The speaker to write the sound to. /// The speaker to write the sound to.
@@ -110,8 +103,7 @@ class Rtttl : public Component {
int samples_count_{0}; int samples_count_{0};
/// The number of samples for the gap between notes. /// The number of samples for the gap between notes.
int samples_gap_{0}; int samples_gap_{0};
#endif // USE_SPEAKER
#endif
/// The callback to call when playback is finished. /// The callback to call when playback is finished.
CallbackManager<void()> on_finished_playback_callback_; CallbackManager<void()> on_finished_playback_callback_;
@@ -145,5 +137,4 @@ class FinishedPlaybackTrigger : public Trigger<> {
} }
}; };
} // namespace rtttl } // namespace esphome::rtttl
} // namespace esphome

View File

@@ -4,6 +4,7 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes> #include <cinttypes>
#include <utility> #include <utility>
@@ -1544,42 +1545,19 @@ void Sprinkler::log_multiplier_zero_warning_(const LogString *method_name) {
ESP_LOGW(TAG, "%s called but multiplier is set to zero; no action taken", LOG_STR_ARG(method_name)); ESP_LOGW(TAG, "%s called but multiplier is set to zero; no action taken", LOG_STR_ARG(method_name));
} }
// Request origin strings indexed by SprinklerValveRunRequestOrigin enum (0-2): USER, CYCLE, QUEUE
PROGMEM_STRING_TABLE(SprinklerRequestOriginStrings, "USER", "CYCLE", "QUEUE", "UNKNOWN");
const LogString *Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) { const LogString *Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) {
switch (origin) { return SprinklerRequestOriginStrings::get_log_str(static_cast<uint8_t>(origin),
case USER: SprinklerRequestOriginStrings::LAST_INDEX);
return LOG_STR("USER");
case CYCLE:
return LOG_STR("CYCLE");
case QUEUE:
return LOG_STR("QUEUE");
default:
return LOG_STR("UNKNOWN");
}
} }
// Sprinkler state strings indexed by SprinklerState enum (0-4): IDLE, STARTING, ACTIVE, STOPPING, BYPASS
PROGMEM_STRING_TABLE(SprinklerStateStrings, "IDLE", "STARTING", "ACTIVE", "STOPPING", "BYPASS", "UNKNOWN");
const LogString *Sprinkler::state_as_str_(SprinklerState state) { const LogString *Sprinkler::state_as_str_(SprinklerState state) {
switch (state) { return SprinklerStateStrings::get_log_str(static_cast<uint8_t>(state), SprinklerStateStrings::LAST_INDEX);
case IDLE:
return LOG_STR("IDLE");
case STARTING:
return LOG_STR("STARTING");
case ACTIVE:
return LOG_STR("ACTIVE");
case STOPPING:
return LOG_STR("STOPPING");
case BYPASS:
return LOG_STR("BYPASS");
default:
return LOG_STR("UNKNOWN");
}
} }
void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {

View File

@@ -1,6 +1,7 @@
#include "ssd1306_base.h" #include "ssd1306_base.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome { namespace esphome {
namespace ssd1306_base { namespace ssd1306_base {
@@ -40,6 +41,55 @@ static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8;
static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC; static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC;
static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD; static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD;
// Verify first enum value and table sizes match SSD1306_MODEL_COUNT
static_assert(SSD1306_MODEL_128_32 == 0, "SSD1306Model enum must start at 0");
// PROGMEM lookup table indexed by SSD1306Model enum (width, height per model)
struct ModelDimensions {
uint8_t width;
uint8_t height;
};
static const ModelDimensions MODEL_DIMS[] PROGMEM = {
{128, 32}, // SSD1306_MODEL_128_32
{128, 64}, // SSD1306_MODEL_128_64
{96, 16}, // SSD1306_MODEL_96_16
{64, 48}, // SSD1306_MODEL_64_48
{64, 32}, // SSD1306_MODEL_64_32
{72, 40}, // SSD1306_MODEL_72_40
{128, 32}, // SH1106_MODEL_128_32
{128, 64}, // SH1106_MODEL_128_64
{96, 16}, // SH1106_MODEL_96_16
{64, 48}, // SH1106_MODEL_64_48
{64, 128}, // SH1107_MODEL_128_64 (note: width is 64, height is 128)
{128, 128}, // SH1107_MODEL_128_128
{128, 32}, // SSD1305_MODEL_128_32
{128, 64}, // SSD1305_MODEL_128_64
};
// clang-format off
PROGMEM_STRING_TABLE(ModelStrings,
"SSD1306 128x32", // SSD1306_MODEL_128_32
"SSD1306 128x64", // SSD1306_MODEL_128_64
"SSD1306 96x16", // SSD1306_MODEL_96_16
"SSD1306 64x48", // SSD1306_MODEL_64_48
"SSD1306 64x32", // SSD1306_MODEL_64_32
"SSD1306 72x40", // SSD1306_MODEL_72_40
"SH1106 128x32", // SH1106_MODEL_128_32
"SH1106 128x64", // SH1106_MODEL_128_64
"SH1106 96x16", // SH1106_MODEL_96_16
"SH1106 64x48", // SH1106_MODEL_64_48
"SH1107 128x64", // SH1107_MODEL_128_64
"SH1107 128x128", // SH1107_MODEL_128_128
"SSD1305 128x32", // SSD1305_MODEL_128_32
"SSD1305 128x64", // SSD1305_MODEL_128_64
"Unknown" // fallback
);
// clang-format on
static_assert(sizeof(MODEL_DIMS) / sizeof(MODEL_DIMS[0]) == SSD1306_MODEL_COUNT,
"MODEL_DIMS must have one entry per SSD1306Model");
static_assert(ModelStrings::COUNT == SSD1306_MODEL_COUNT + 1,
"ModelStrings must have one entry per SSD1306Model plus fallback");
void SSD1306::setup() { void SSD1306::setup() {
this->init_internal_(this->get_buffer_length_()); this->init_internal_(this->get_buffer_length_());
@@ -146,6 +196,7 @@ void SSD1306::setup() {
break; break;
case SH1107_MODEL_128_64: case SH1107_MODEL_128_64:
case SH1107_MODEL_128_128: case SH1107_MODEL_128_128:
case SSD1306_MODEL_COUNT:
// Not used, but prevents build warning // Not used, but prevents build warning
break; break;
} }
@@ -274,54 +325,14 @@ void SSD1306::turn_off() {
this->is_on_ = false; this->is_on_ = false;
} }
int SSD1306::get_height_internal() { int SSD1306::get_height_internal() {
switch (this->model_) { if (this->model_ >= SSD1306_MODEL_COUNT)
case SH1107_MODEL_128_64: return 0;
case SH1107_MODEL_128_128: return progmem_read_byte(&MODEL_DIMS[this->model_].height);
return 128;
case SSD1306_MODEL_128_32:
case SSD1306_MODEL_64_32:
case SH1106_MODEL_128_32:
case SSD1305_MODEL_128_32:
return 32;
case SSD1306_MODEL_128_64:
case SH1106_MODEL_128_64:
case SSD1305_MODEL_128_64:
return 64;
case SSD1306_MODEL_96_16:
case SH1106_MODEL_96_16:
return 16;
case SSD1306_MODEL_64_48:
case SH1106_MODEL_64_48:
return 48;
case SSD1306_MODEL_72_40:
return 40;
default:
return 0;
}
} }
int SSD1306::get_width_internal() { int SSD1306::get_width_internal() {
switch (this->model_) { if (this->model_ >= SSD1306_MODEL_COUNT)
case SSD1306_MODEL_128_32: return 0;
case SH1106_MODEL_128_32: return progmem_read_byte(&MODEL_DIMS[this->model_].width);
case SSD1306_MODEL_128_64:
case SH1106_MODEL_128_64:
case SSD1305_MODEL_128_32:
case SSD1305_MODEL_128_64:
case SH1107_MODEL_128_128:
return 128;
case SSD1306_MODEL_96_16:
case SH1106_MODEL_96_16:
return 96;
case SSD1306_MODEL_64_48:
case SSD1306_MODEL_64_32:
case SH1106_MODEL_64_48:
case SH1107_MODEL_128_64:
return 64;
case SSD1306_MODEL_72_40:
return 72;
default:
return 0;
}
} }
size_t SSD1306::get_buffer_length_() { size_t SSD1306::get_buffer_length_() {
return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u;
@@ -361,37 +372,8 @@ void SSD1306::init_reset_() {
this->reset_pin_->digital_write(true); this->reset_pin_->digital_write(true);
} }
} }
const char *SSD1306::model_str_() { const LogString *SSD1306::model_str_() {
switch (this->model_) { return ModelStrings::get_log_str(static_cast<uint8_t>(this->model_), ModelStrings::LAST_INDEX);
case SSD1306_MODEL_128_32:
return "SSD1306 128x32";
case SSD1306_MODEL_128_64:
return "SSD1306 128x64";
case SSD1306_MODEL_64_32:
return "SSD1306 64x32";
case SSD1306_MODEL_96_16:
return "SSD1306 96x16";
case SSD1306_MODEL_64_48:
return "SSD1306 64x48";
case SSD1306_MODEL_72_40:
return "SSD1306 72x40";
case SH1106_MODEL_128_32:
return "SH1106 128x32";
case SH1106_MODEL_128_64:
return "SH1106 128x64";
case SH1106_MODEL_96_16:
return "SH1106 96x16";
case SH1106_MODEL_64_48:
return "SH1106 64x48";
case SH1107_MODEL_128_64:
return "SH1107 128x64";
case SSD1305_MODEL_128_32:
return "SSD1305 128x32";
case SSD1305_MODEL_128_64:
return "SSD1305 128x64";
default:
return "Unknown";
}
} }
} // namespace ssd1306_base } // namespace ssd1306_base

View File

@@ -22,6 +22,9 @@ enum SSD1306Model {
SH1107_MODEL_128_128, SH1107_MODEL_128_128,
SSD1305_MODEL_128_32, SSD1305_MODEL_128_32,
SSD1305_MODEL_128_64, SSD1305_MODEL_128_64,
// When adding a new model, add it before SSD1306_MODEL_COUNT and update
// MODEL_DIMS and ModelStrings tables in ssd1306_base.cpp
SSD1306_MODEL_COUNT, // must be last
}; };
class SSD1306 : public display::DisplayBuffer { class SSD1306 : public display::DisplayBuffer {
@@ -70,7 +73,7 @@ class SSD1306 : public display::DisplayBuffer {
int get_height_internal() override; int get_height_internal() override;
int get_width_internal() override; int get_width_internal() override;
size_t get_buffer_length_(); size_t get_buffer_length_();
const char *model_str_(); const LogString *model_str_();
SSD1306Model model_{SSD1306_MODEL_128_64}; SSD1306Model model_{SSD1306_MODEL_128_64};
GPIOPin *reset_pin_{nullptr}; GPIOPin *reset_pin_{nullptr};

View File

@@ -28,7 +28,7 @@ void I2CSSD1306::dump_config() {
" Offset X: %d\n" " Offset X: %d\n"
" Offset Y: %d\n" " Offset Y: %d\n"
" Inverted Color: %s", " Inverted Color: %s",
this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), LOG_STR_ARG(this->model_str_()), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_),
this->offset_x_, this->offset_y_, YESNO(this->invert_)); this->offset_x_, this->offset_y_, YESNO(this->invert_));
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_);

View File

@@ -24,7 +24,7 @@ void SPISSD1306::dump_config() {
" Offset X: %d\n" " Offset X: %d\n"
" Offset Y: %d\n" " Offset Y: %d\n"
" Inverted Color: %s", " Inverted Color: %s",
this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), LOG_STR_ARG(this->model_str_()), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_),
this->offset_x_, this->offset_y_, YESNO(this->invert_)); this->offset_x_, this->offset_y_, YESNO(this->invert_));
LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_);

View File

@@ -344,14 +344,15 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
memcpy(user_info + user_len + 1, password, pass_len); memcpy(user_info + user_len + 1, password, pass_len);
user_info[user_info_len] = '\0'; user_info[user_info_len] = '\0';
size_t n = 0, out; // Base64 output size is ceil(input_len * 4/3) + 1, with input bounded to 256 bytes
esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast<const uint8_t *>(user_info), user_info_len); // max output is ceil(256 * 4/3) + 1 = 343 bytes, use 350 for safety
constexpr size_t max_digest_len = 350;
auto digest = std::unique_ptr<char[]>(new char[n + 1]); char digest[max_digest_len];
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest.get()), n, &out, size_t out;
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
reinterpret_cast<const uint8_t *>(user_info), user_info_len); reinterpret_cast<const uint8_t *>(user_info), user_info_len);
return strcmp(digest.get(), auth_str + auth_prefix_len) == 0; return strcmp(digest, auth_str + auth_prefix_len) == 0;
} }
void AsyncWebServerRequest::requestAuthentication(const char *realm) const { void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
@@ -861,12 +862,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
} }
}); });
// Process data // Process data - use stack buffer to avoid heap allocation
std::unique_ptr<char[]> buffer(new char[MULTIPART_CHUNK_SIZE]); char buffer[MULTIPART_CHUNK_SIZE];
size_t bytes_since_yield = 0; size_t bytes_since_yield = 0;
for (size_t remaining = r->content_len; remaining > 0;) { for (size_t remaining = r->content_len; remaining > 0;) {
int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE)); int recv_len = httpd_req_recv(r, buffer, std::min(remaining, MULTIPART_CHUNK_SIZE));
if (recv_len <= 0) { if (recv_len <= 0) {
httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST, httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
@@ -874,7 +875,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL; return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
} }
if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) { if (reader->parse(buffer, recv_len) != static_cast<size_t>(recv_len)) {
ESP_LOGW(TAG, "Multipart parser error"); ESP_LOGW(TAG, "Multipart parser error");
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
return ESP_FAIL; return ESP_FAIL;

View File

@@ -204,36 +204,40 @@ void Application::loop() {
this->last_loop_ = last_op_end_time; this->last_loop_ = last_op_end_time;
if (this->dump_config_at_ < this->components_.size()) { if (this->dump_config_at_ < this->components_.size()) {
if (this->dump_config_at_ == 0) { this->process_dump_config_();
char build_time_str[Application::BUILD_TIME_STR_SIZE]; }
this->get_build_time_string(build_time_str); }
ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", build_time_str);
void Application::process_dump_config_() {
if (this->dump_config_at_ == 0) {
char build_time_str[Application::BUILD_TIME_STR_SIZE];
this->get_build_time_string(build_time_str);
ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", build_time_str);
#ifdef ESPHOME_PROJECT_NAME #ifdef ESPHOME_PROJECT_NAME
ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION); ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION);
#endif #endif
#ifdef USE_ESP32 #ifdef USE_ESP32
esp_chip_info_t chip_info; esp_chip_info_t chip_info;
esp_chip_info(&chip_info); esp_chip_info(&chip_info);
ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, 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); chip_info.revision % 100, chip_info.cores);
#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) #if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET)
// Suggest optimization for chips that don't need the PSRAM cache workaround // Suggest optimization for chips that don't need the PSRAM cache workaround
if (chip_info.revision >= 300) { if (chip_info.revision >= 300) {
#ifdef USE_PSRAM #ifdef USE_PSRAM
ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100, ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100,
chip_info.revision % 100); chip_info.revision % 100);
#else #else
ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100,
chip_info.revision % 100); chip_info.revision % 100);
#endif
}
#endif
#endif #endif
} }
#endif
this->components_[this->dump_config_at_]->call_dump_config(); #endif
this->dump_config_at_++;
} }
this->components_[this->dump_config_at_]->call_dump_config();
this->dump_config_at_++;
} }
void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) {

View File

@@ -519,6 +519,11 @@ class Application {
void before_loop_tasks_(uint32_t loop_start_time); void before_loop_tasks_(uint32_t loop_start_time);
void after_loop_tasks_(); void after_loop_tasks_();
/// Process dump_config output one component per loop iteration.
/// Extracted from loop() to keep cold startup/reconnect logging out of the hot path.
/// Caller must ensure dump_config_at_ < components_.size().
void __attribute__((noinline)) process_dump_config_();
void feed_wdt_arch_(); void feed_wdt_arch_();
/// Perform a delay while also monitoring socket file descriptors for readiness /// Perform a delay while also monitoring socket file descriptors for readiness

View File

@@ -26,6 +26,7 @@ class ComponentIterator {
public: public:
void begin(bool include_internal = false); void begin(bool include_internal = false);
void advance(); void advance();
bool completed() const { return this->state_ == IteratorState::NONE; }
virtual bool on_begin(); virtual bool on_begin();
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0; virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0;

View File

@@ -107,6 +107,24 @@ static void validate_static_string(const char *name) {
// iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to // iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to
// avoid the main thread modifying the list while it is being accessed. // avoid the main thread modifying the list while it is being accessed.
// Calculate random offset for interval timers
// Extracted from set_timer_common_ to reduce code size - float math + random_float()
// only needed for intervals, not timeouts
uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) {
return static_cast<uint32_t>(std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float());
}
// Check if a retry was already cancelled in items_ or to_add_
// Extracted from set_timer_common_ to reduce code size - retry path is cold and deprecated
// Remove before 2026.8.0 along with all retry code
bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name,
uint32_t hash_or_id) {
return has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id,
/* match_retry= */ true) ||
has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id,
/* match_retry= */ true);
}
// Common implementation for both timeout and interval // Common implementation for both timeout and interval
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type,
@@ -130,84 +148,66 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
// Create and populate the scheduler item // Create and populate the scheduler item
auto item = this->get_item_from_pool_locked_(); auto item = this->get_item_from_pool_locked_();
item->component = component; item->component = component;
switch (name_type) { item->set_name(name_type, static_name, hash_or_id);
case NameType::STATIC_STRING:
item->set_static_name(static_name);
break;
case NameType::HASHED_STRING:
item->set_hashed_name(hash_or_id);
break;
case NameType::NUMERIC_ID:
item->set_numeric_id(hash_or_id);
break;
case NameType::NUMERIC_ID_INTERNAL:
item->set_internal_id(hash_or_id);
break;
}
item->type = type; item->type = type;
item->callback = std::move(func); item->callback = std::move(func);
// Reset remove flag - recycled items may have been cancelled (remove=true) in previous use // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use
this->set_item_removed_(item.get(), false); this->set_item_removed_(item.get(), false);
item->is_retry = is_retry; item->is_retry = is_retry;
// Determine target container: defer_queue_ for deferred items, to_add_ for everything else.
// Using a pointer lets both paths share the cancel + push_back epilogue.
auto *target = &this->to_add_;
#ifndef ESPHOME_THREAD_SINGLE #ifndef ESPHOME_THREAD_SINGLE
// Special handling for defer() (delay = 0, type = TIMEOUT) // Special handling for defer() (delay = 0, type = TIMEOUT)
// Single-core platforms don't need thread-safe defer handling // Single-core platforms don't need thread-safe defer handling
if (delay == 0 && type == SchedulerItem::TIMEOUT) { if (delay == 0 && type == SchedulerItem::TIMEOUT) {
// Put in defer queue for guaranteed FIFO execution // Put in defer queue for guaranteed FIFO execution
if (!skip_cancel) { target = &this->defer_queue_;
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } else
}
this->defer_queue_.push_back(std::move(item));
return;
}
#endif /* not ESPHOME_THREAD_SINGLE */ #endif /* not ESPHOME_THREAD_SINGLE */
{
// Type-specific setup // Type-specific setup
if (type == SchedulerItem::INTERVAL) { if (type == SchedulerItem::INTERVAL) {
item->interval = delay; item->interval = delay;
// first execution happens immediately after a random smallish offset // first execution happens immediately after a random smallish offset
// Calculate random offset (0 to min(interval/2, 5s)) uint32_t offset = this->calculate_interval_offset_(delay);
uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); item->set_next_execution(now + offset);
item->set_next_execution(now + offset);
#ifdef ESPHOME_LOG_HAS_VERBOSE #ifdef ESPHOME_LOG_HAS_VERBOSE
SchedulerNameLog name_log; SchedulerNameLog name_log;
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms",
name_log.format(name_type, static_name, hash_or_id), delay, offset); name_log.format(name_type, static_name, hash_or_id), delay, offset);
#endif #endif
} else { } else {
item->interval = 0; item->interval = 0;
item->set_next_execution(now + delay); item->set_next_execution(now + delay);
} }
#ifdef ESPHOME_DEBUG_SCHEDULER #ifdef ESPHOME_DEBUG_SCHEDULER
this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now);
#endif /* ESPHOME_DEBUG_SCHEDULER */ #endif /* ESPHOME_DEBUG_SCHEDULER */
// For retries, check if there's a cancelled timeout first // For retries, check if there's a cancelled timeout first
// Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name
if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && type == SchedulerItem::TIMEOUT && if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) &&
(has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, type == SchedulerItem::TIMEOUT &&
/* match_retry= */ true) || this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) {
has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, // Skip scheduling - the retry was cancelled
/* match_retry= */ true))) {
// Skip scheduling - the retry was cancelled
#ifdef ESPHOME_DEBUG_SCHEDULER #ifdef ESPHOME_DEBUG_SCHEDULER
SchedulerNameLog skip_name_log; SchedulerNameLog skip_name_log;
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item",
skip_name_log.format(name_type, static_name, hash_or_id)); skip_name_log.format(name_type, static_name, hash_or_id));
#endif #endif
return; return;
}
} }
// If name is provided, do atomic cancel-and-add (unless skip_cancel is true) // Common epilogue: atomic cancel-and-add (unless skip_cancel is true)
// Cancel existing items
if (!skip_cancel) { if (!skip_cancel) {
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type);
} }
// Add new item directly to to_add_ target->push_back(std::move(item));
// since we have the lock held
this->to_add_.push_back(std::move(item));
} }
void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func) { void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func) {

View File

@@ -219,28 +219,15 @@ class Scheduler {
// Helper to get the name type // Helper to get the name type
NameType get_name_type() const { return name_type_; } NameType get_name_type() const { return name_type_; }
// Helper to set a static string name (no allocation) // Set name storage: for STATIC_STRING stores the pointer, for all other types stores hash_or_id.
void set_static_name(const char *name) { // Both union members occupy the same offset, so only one store is needed.
name_.static_name = name; void set_name(NameType type, const char *static_name, uint32_t hash_or_id) {
name_type_ = NameType::STATIC_STRING; if (type == NameType::STATIC_STRING) {
} name_.static_name = static_name;
} else {
// Helper to set a hashed string name (hash computed from std::string) name_.hash_or_id = hash_or_id;
void set_hashed_name(uint32_t hash) { }
name_.hash_or_id = hash; name_type_ = type;
name_type_ = NameType::HASHED_STRING;
}
// Helper to set a numeric ID name
void set_numeric_id(uint32_t id) {
name_.hash_or_id = id;
name_type_ = NameType::NUMERIC_ID;
}
// Helper to set an internal numeric ID (separate namespace from NUMERIC_ID)
void set_internal_id(uint32_t id) {
name_.hash_or_id = id;
name_type_ = NameType::NUMERIC_ID_INTERNAL;
} }
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b); static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
@@ -355,6 +342,17 @@ class Scheduler {
// Helper to perform full cleanup when too many items are cancelled // Helper to perform full cleanup when too many items are cancelled
void full_cleanup_removed_items_(); void full_cleanup_removed_items_();
// Helper to calculate random offset for interval timers - extracted to reduce code size of set_timer_common_
// IMPORTANT: Must not be inlined - called only for intervals, keeping it out of the hot path saves flash.
uint32_t __attribute__((noinline)) calculate_interval_offset_(uint32_t delay);
// Helper to check if a retry was already cancelled - extracted to reduce code size of set_timer_common_
// Remove before 2026.8.0 along with all retry code.
// IMPORTANT: Must not be inlined - retry path is cold and deprecated.
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
bool __attribute__((noinline))
is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
#ifdef ESPHOME_DEBUG_SCHEDULER #ifdef ESPHOME_DEBUG_SCHEDULER
// Helper for debug logging in set_timer_common_ - extracted to reduce code size // Helper for debug logging in set_timer_common_ - extracted to reduce code size
void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id, void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id,

View File

@@ -133,6 +133,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
) )
# Suppress Python syntax warnings from third-party scripts during compilation # Suppress Python syntax warnings from third-party scripts during compilation
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
# Increase uv retry count to handle transient network errors (default is 3)
os.environ.setdefault("UV_HTTP_RETRIES", "10")
cmd = ["platformio"] + list(args) cmd = ["platformio"] + list(args)
if not CORE.verbose: if not CORE.verbose:

View File

@@ -214,7 +214,7 @@ build_unflags =
; This are common settings for the LibreTiny (all variants) using Arduino. ; This are common settings for the LibreTiny (all variants) using Arduino.
[common:libretiny-arduino] [common:libretiny-arduino]
extends = common:arduino extends = common:arduino
platform = https://github.com/libretiny-eu/libretiny.git#v1.11.0 platform = https://github.com/libretiny-eu/libretiny.git#v1.12.1
framework = arduino framework = arduino
lib_compat_mode = soft lib_compat_mode = soft
lib_deps = lib_deps =

View File

@@ -6,8 +6,8 @@ esp32:
type: esp-idf type: esp-idf
components: components:
- espressif/mdns^1.8.2 - espressif/mdns^1.8.2
- name: espressif/esp_hosted - name: espressif/button
ref: 2.7.0 ref: 4.1.5
advanced: advanced:
enable_idf_experimental_features: yes enable_idf_experimental_features: yes
disable_debug_stubs: true disable_debug_stubs: true