mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge remote-tracking branch 'origin/logger_strlen' into logger_strlen
This commit is contained in:
		| @@ -442,6 +442,7 @@ esphome/components/sun/* @OttoWinter | |||||||
| esphome/components/sun_gtil2/* @Mat931 | esphome/components/sun_gtil2/* @Mat931 | ||||||
| esphome/components/switch/* @esphome/core | esphome/components/switch/* @esphome/core | ||||||
| esphome/components/switch/binary_sensor/* @ssieb | esphome/components/switch/binary_sensor/* @ssieb | ||||||
|  | esphome/components/sx126x/* @swoboda1337 | ||||||
| esphome/components/sx127x/* @swoboda1337 | esphome/components/sx127x/* @swoboda1337 | ||||||
| esphome/components/syslog/* @clydebarrow | esphome/components/syslog/* @clydebarrow | ||||||
| esphome/components/t6615/* @tylermenezes | esphome/components/t6615/* @tylermenezes | ||||||
|   | |||||||
| @@ -52,7 +52,12 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) | |||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| static constexpr size_t FLUSH_BATCH_SIZE = 8; | // Batch size for BLE advertisements to maximize WiFi efficiency | ||||||
|  | // Each advertisement is up to 80 bytes when packaged (including protocol overhead) | ||||||
|  | // Most advertisements are 20-30 bytes, allowing even more to fit per packet | ||||||
|  | // 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload | ||||||
|  | // This achieves ~97% WiFi MTU utilization while staying under the limit | ||||||
|  | static constexpr size_t FLUSH_BATCH_SIZE = 16; | ||||||
| static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { | static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { | ||||||
|   static std::vector<api::BluetoothLERawAdvertisement> batch_buffer; |   static std::vector<api::BluetoothLERawAdvertisement> batch_buffer; | ||||||
|   return batch_buffer; |   return batch_buffer; | ||||||
|   | |||||||
							
								
								
									
										69
									
								
								esphome/components/esp32/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								esphome/components/esp32/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
|  | #include "esp_efuse.h" | ||||||
|  | #include "esp_efuse_table.h" | ||||||
|  | #include "esp_mac.h" | ||||||
|  |  | ||||||
|  | #include <freertos/FreeRTOS.h> | ||||||
|  | #include <freertos/portmacro.h> | ||||||
|  | #include "esp_random.h" | ||||||
|  | #include "esp_system.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  |  | ||||||
|  | uint32_t random_uint32() { return esp_random(); } | ||||||
|  | bool random_bytes(uint8_t *data, size_t len) { | ||||||
|  |   esp_fill_random(data, len); | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } | ||||||
|  | Mutex::~Mutex() {} | ||||||
|  | void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } | ||||||
|  | bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } | ||||||
|  | void Mutex::unlock() { xSemaphoreGive(this->handle_); } | ||||||
|  |  | ||||||
|  | // only affects the executing core | ||||||
|  | // so should not be used as a mutex lock, only to get accurate timing | ||||||
|  | IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } | ||||||
|  | IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } | ||||||
|  |  | ||||||
|  | void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) | ||||||
|  | #if defined(CONFIG_SOC_IEEE802154_SUPPORTED) | ||||||
|  |   // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default | ||||||
|  |   // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead. | ||||||
|  |   if (has_custom_mac_address()) { | ||||||
|  |     esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48); | ||||||
|  |   } else { | ||||||
|  |     esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); | ||||||
|  |   } | ||||||
|  | #else | ||||||
|  |   if (has_custom_mac_address()) { | ||||||
|  |     esp_efuse_mac_get_custom(mac); | ||||||
|  |   } else { | ||||||
|  |     esp_efuse_mac_get_default(mac); | ||||||
|  |   } | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } | ||||||
|  |  | ||||||
|  | bool has_custom_mac_address() { | ||||||
|  | #if !defined(USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC) | ||||||
|  |   uint8_t mac[6]; | ||||||
|  |   // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails | ||||||
|  | #ifndef USE_ESP32_VARIANT_ESP32 | ||||||
|  |   return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); | ||||||
|  | #else | ||||||
|  |   return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); | ||||||
|  | #endif | ||||||
|  | #else | ||||||
|  |   return false; | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // USE_ESP32 | ||||||
| @@ -25,10 +25,15 @@ namespace esphome { | |||||||
| namespace esp32_ble { | namespace esp32_ble { | ||||||
|  |  | ||||||
| // Maximum number of BLE scan results to buffer | // Maximum number of BLE scan results to buffer | ||||||
|  | // Sized to handle bursts of advertisements while allowing for processing delays | ||||||
|  | // With 16 advertisements per batch and some safety margin: | ||||||
|  | // - Without PSRAM: 24 entries (1.5× batch size) | ||||||
|  | // - With PSRAM: 36 entries (2.25× batch size) | ||||||
|  | // The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers | ||||||
| #ifdef USE_PSRAM | #ifdef USE_PSRAM | ||||||
| static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32; | static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36; | ||||||
| #else | #else | ||||||
| static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20; | static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| // Maximum size of the BLE event queue - must be power of 2 for lock-free queue | // Maximum size of the BLE event queue - must be power of 2 for lock-free queue | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								esphome/components/esp8266/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								esphome/components/esp8266/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  |  | ||||||
|  | #include <osapi.h> | ||||||
|  | #include <user_interface.h> | ||||||
|  | // for xt_rsil()/xt_wsr_ps() | ||||||
|  | #include <Arduino.h> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  |  | ||||||
|  | uint32_t random_uint32() { return os_random(); } | ||||||
|  | bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; } | ||||||
|  |  | ||||||
|  | // ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. | ||||||
|  | Mutex::Mutex() {} | ||||||
|  | Mutex::~Mutex() {} | ||||||
|  | void Mutex::lock() {} | ||||||
|  | bool Mutex::try_lock() { return true; } | ||||||
|  | void Mutex::unlock() {} | ||||||
|  |  | ||||||
|  | IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } | ||||||
|  | IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } | ||||||
|  |  | ||||||
|  | void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) | ||||||
|  |   wifi_get_macaddr(STATION_IF, mac); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // USE_ESP8266 | ||||||
							
								
								
									
										57
									
								
								esphome/components/host/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								esphome/components/host/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_HOST | ||||||
|  |  | ||||||
|  | #ifndef _WIN32 | ||||||
|  | #include <net/if.h> | ||||||
|  | #include <netinet/in.h> | ||||||
|  | #include <sys/ioctl.h> | ||||||
|  | #endif | ||||||
|  | #include <unistd.h> | ||||||
|  | #include <limits> | ||||||
|  | #include <random> | ||||||
|  |  | ||||||
|  | #include "esphome/core/defines.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "helpers.host"; | ||||||
|  |  | ||||||
|  | uint32_t random_uint32() { | ||||||
|  |   std::random_device dev; | ||||||
|  |   std::mt19937 rng(dev()); | ||||||
|  |   std::uniform_int_distribution<uint32_t> dist(0, std::numeric_limits<uint32_t>::max()); | ||||||
|  |   return dist(rng); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool random_bytes(uint8_t *data, size_t len) { | ||||||
|  |   FILE *fp = fopen("/dev/urandom", "r"); | ||||||
|  |   if (fp == nullptr) { | ||||||
|  |     ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno); | ||||||
|  |     exit(1); | ||||||
|  |   } | ||||||
|  |   size_t read = fread(data, 1, len, fp); | ||||||
|  |   if (read != len) { | ||||||
|  |     ESP_LOGW(TAG, "Not enough data from /dev/urandom"); | ||||||
|  |     exit(1); | ||||||
|  |   } | ||||||
|  |   fclose(fp); | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Host platform uses std::mutex for proper thread synchronization | ||||||
|  | Mutex::Mutex() { handle_ = new std::mutex(); } | ||||||
|  | Mutex::~Mutex() { delete static_cast<std::mutex *>(handle_); } | ||||||
|  | void Mutex::lock() { static_cast<std::mutex *>(handle_)->lock(); } | ||||||
|  | bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock(); } | ||||||
|  | void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); } | ||||||
|  |  | ||||||
|  | void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) | ||||||
|  |   static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS; | ||||||
|  |   memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // USE_HOST | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| from esphome import pins | from esphome import pins | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.components import display, i2c | from esphome.components import display, i2c | ||||||
|  | from esphome.components.esp32 import CONF_CPU_FREQUENCY | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_FULL_UPDATE_EVERY, |     CONF_FULL_UPDATE_EVERY, | ||||||
| @@ -13,7 +14,9 @@ from esphome.const import ( | |||||||
|     CONF_PAGES, |     CONF_PAGES, | ||||||
|     CONF_TRANSFORM, |     CONF_TRANSFORM, | ||||||
|     CONF_WAKEUP_PIN, |     CONF_WAKEUP_PIN, | ||||||
|  |     PLATFORM_ESP32, | ||||||
| ) | ) | ||||||
|  | import esphome.final_validate as fv | ||||||
|  |  | ||||||
| DEPENDENCIES = ["i2c", "esp32"] | DEPENDENCIES = ["i2c", "esp32"] | ||||||
| AUTO_LOAD = ["psram"] | AUTO_LOAD = ["psram"] | ||||||
| @@ -120,6 +123,18 @@ CONFIG_SCHEMA = cv.All( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _validate_cpu_frequency(config): | ||||||
|  |     esp32_config = fv.full_config.get()[PLATFORM_ESP32] | ||||||
|  |     if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ": | ||||||
|  |         raise cv.Invalid( | ||||||
|  |             "Inkplate requires 240MHz CPU frequency (set in esp32 component)" | ||||||
|  |         ) | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,13 +13,13 @@ from esphome.const import ( | |||||||
|  |  | ||||||
| from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns | from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns | ||||||
|  |  | ||||||
| ResetButton = ld2450_ns.class_("ResetButton", button.Button) | FactoryResetButton = ld2450_ns.class_("FactoryResetButton", button.Button) | ||||||
| RestartButton = ld2450_ns.class_("RestartButton", button.Button) | RestartButton = ld2450_ns.class_("RestartButton", button.Button) | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = { | CONFIG_SCHEMA = { | ||||||
|     cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), |     cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), | ||||||
|     cv.Optional(CONF_FACTORY_RESET): button.button_schema( |     cv.Optional(CONF_FACTORY_RESET): button.button_schema( | ||||||
|         ResetButton, |         FactoryResetButton, | ||||||
|         device_class=DEVICE_CLASS_RESTART, |         device_class=DEVICE_CLASS_RESTART, | ||||||
|         entity_category=ENTITY_CATEGORY_CONFIG, |         entity_category=ENTITY_CATEGORY_CONFIG, | ||||||
|         icon=ICON_RESTART_ALERT, |         icon=ICON_RESTART_ALERT, | ||||||
| @@ -38,7 +38,7 @@ async def to_code(config): | |||||||
|     if factory_reset_config := config.get(CONF_FACTORY_RESET): |     if factory_reset_config := config.get(CONF_FACTORY_RESET): | ||||||
|         b = await button.new_button(factory_reset_config) |         b = await button.new_button(factory_reset_config) | ||||||
|         await cg.register_parented(b, config[CONF_LD2450_ID]) |         await cg.register_parented(b, config[CONF_LD2450_ID]) | ||||||
|         cg.add(ld2450_component.set_reset_button(b)) |         cg.add(ld2450_component.set_factory_reset_button(b)) | ||||||
|     if restart_config := config.get(CONF_RESTART): |     if restart_config := config.get(CONF_RESTART): | ||||||
|         b = await button.new_button(restart_config) |         b = await button.new_button(restart_config) | ||||||
|         await cg.register_parented(b, config[CONF_LD2450_ID]) |         await cg.register_parented(b, config[CONF_LD2450_ID]) | ||||||
|   | |||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | #include "factory_reset_button.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace ld2450 { | ||||||
|  |  | ||||||
|  | void FactoryResetButton::press_action() { this->parent_->factory_reset(); } | ||||||
|  |  | ||||||
|  | }  // namespace ld2450 | ||||||
|  | }  // namespace esphome | ||||||
| @@ -6,9 +6,9 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace ld2450 { | namespace ld2450 { | ||||||
| 
 | 
 | ||||||
| class ResetButton : public button::Button, public Parented<LD2450Component> { | class FactoryResetButton : public button::Button, public Parented<LD2450Component> { | ||||||
|  public: |  public: | ||||||
|   ResetButton() = default; |   FactoryResetButton() = default; | ||||||
| 
 | 
 | ||||||
|  protected: |  protected: | ||||||
|   void press_action() override; |   void press_action() override; | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| #include "reset_button.h" |  | ||||||
|  |  | ||||||
| namespace esphome { |  | ||||||
| namespace ld2450 { |  | ||||||
|  |  | ||||||
| void ResetButton::press_action() { this->parent_->factory_reset(); } |  | ||||||
|  |  | ||||||
| }  // namespace ld2450 |  | ||||||
| }  // namespace esphome |  | ||||||
| @@ -18,11 +18,10 @@ namespace esphome { | |||||||
| namespace ld2450 { | namespace ld2450 { | ||||||
|  |  | ||||||
| static const char *const TAG = "ld2450"; | static const char *const TAG = "ld2450"; | ||||||
| static const char *const NO_MAC = "08:05:04:03:02:01"; |  | ||||||
| static const char *const UNKNOWN_MAC = "unknown"; | static const char *const UNKNOWN_MAC = "unknown"; | ||||||
| static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; | static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; | ||||||
|  |  | ||||||
| enum BaudRateStructure : uint8_t { | enum BaudRate : uint8_t { | ||||||
|   BAUD_RATE_9600 = 1, |   BAUD_RATE_9600 = 1, | ||||||
|   BAUD_RATE_19200 = 2, |   BAUD_RATE_19200 = 2, | ||||||
|   BAUD_RATE_38400 = 3, |   BAUD_RATE_38400 = 3, | ||||||
| @@ -33,14 +32,13 @@ enum BaudRateStructure : uint8_t { | |||||||
|   BAUD_RATE_460800 = 8 |   BAUD_RATE_460800 = 8 | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // Zone type struct | enum ZoneType : uint8_t { | ||||||
| enum ZoneTypeStructure : uint8_t { |  | ||||||
|   ZONE_DISABLED = 0, |   ZONE_DISABLED = 0, | ||||||
|   ZONE_DETECTION = 1, |   ZONE_DETECTION = 1, | ||||||
|   ZONE_FILTER = 2, |   ZONE_FILTER = 2, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum PeriodicDataStructure : uint8_t { | enum PeriodicData : uint8_t { | ||||||
|   TARGET_X = 4, |   TARGET_X = 4, | ||||||
|   TARGET_Y = 6, |   TARGET_Y = 6, | ||||||
|   TARGET_SPEED = 8, |   TARGET_SPEED = 8, | ||||||
| @@ -48,12 +46,12 @@ enum PeriodicDataStructure : uint8_t { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| enum PeriodicDataValue : uint8_t { | enum PeriodicDataValue : uint8_t { | ||||||
|   HEAD = 0xAA, |   HEADER = 0xAA, | ||||||
|   END = 0x55, |   FOOTER = 0x55, | ||||||
|   CHECK = 0x00, |   CHECK = 0x00, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum AckDataStructure : uint8_t { | enum AckData : uint8_t { | ||||||
|   COMMAND = 6, |   COMMAND = 6, | ||||||
|   COMMAND_STATUS = 7, |   COMMAND_STATUS = 7, | ||||||
| }; | }; | ||||||
| @@ -61,11 +59,11 @@ enum AckDataStructure : uint8_t { | |||||||
| // Memory-efficient lookup tables | // Memory-efficient lookup tables | ||||||
| struct StringToUint8 { | struct StringToUint8 { | ||||||
|   const char *str; |   const char *str; | ||||||
|   uint8_t value; |   const uint8_t value; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| struct Uint8ToString { | struct Uint8ToString { | ||||||
|   uint8_t value; |   const uint8_t value; | ||||||
|   const char *str; |   const char *str; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -75,6 +73,13 @@ constexpr StringToUint8 BAUD_RATES_BY_STR[] = { | |||||||
|     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, |     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | constexpr Uint8ToString DIRECTION_BY_UINT[] = { | ||||||
|  |     {DIRECTION_APPROACHING, "Approaching"}, | ||||||
|  |     {DIRECTION_MOVING_AWAY, "Moving away"}, | ||||||
|  |     {DIRECTION_STATIONARY, "Stationary"}, | ||||||
|  |     {DIRECTION_NA, "NA"}, | ||||||
|  | }; | ||||||
|  |  | ||||||
| constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = { | constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = { | ||||||
|     {ZONE_DISABLED, "Disabled"}, |     {ZONE_DISABLED, "Disabled"}, | ||||||
|     {ZONE_DETECTION, "Detection"}, |     {ZONE_DETECTION, "Detection"}, | ||||||
| @@ -104,28 +109,35 @@ template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t v | |||||||
|   return "";  // Not found |   return "";  // Not found | ||||||
| } | } | ||||||
|  |  | ||||||
| // LD2450 serial command header & footer |  | ||||||
| static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; |  | ||||||
| static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; |  | ||||||
| // LD2450 UART Serial Commands | // LD2450 UART Serial Commands | ||||||
| static const uint8_t CMD_ENABLE_CONF = 0xFF; | static constexpr uint8_t CMD_ENABLE_CONF = 0xFF; | ||||||
| static const uint8_t CMD_DISABLE_CONF = 0xFE; | static constexpr uint8_t CMD_DISABLE_CONF = 0xFE; | ||||||
| static const uint8_t CMD_VERSION = 0xA0; | static constexpr uint8_t CMD_QUERY_VERSION = 0xA0; | ||||||
| static const uint8_t CMD_MAC = 0xA5; | static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5; | ||||||
| static const uint8_t CMD_RESET = 0xA2; | static constexpr uint8_t CMD_RESET = 0xA2; | ||||||
| static const uint8_t CMD_RESTART = 0xA3; | static constexpr uint8_t CMD_RESTART = 0xA3; | ||||||
| static const uint8_t CMD_BLUETOOTH = 0xA4; | static constexpr uint8_t CMD_BLUETOOTH = 0xA4; | ||||||
| static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80; | static constexpr uint8_t CMD_SINGLE_TARGET_MODE = 0x80; | ||||||
| static const uint8_t CMD_MULTI_TARGET_MODE = 0x90; | static constexpr uint8_t CMD_MULTI_TARGET_MODE = 0x90; | ||||||
| static const uint8_t CMD_QUERY_TARGET_MODE = 0x91; | static constexpr uint8_t CMD_QUERY_TARGET_MODE = 0x91; | ||||||
| static const uint8_t CMD_SET_BAUD_RATE = 0xA1; | static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1; | ||||||
| static const uint8_t CMD_QUERY_ZONE = 0xC1; | static constexpr uint8_t CMD_QUERY_ZONE = 0xC1; | ||||||
| static const uint8_t CMD_SET_ZONE = 0xC2; | static constexpr uint8_t CMD_SET_ZONE = 0xC2; | ||||||
|  | // Header & Footer size | ||||||
|  | static constexpr uint8_t HEADER_FOOTER_SIZE = 4; | ||||||
|  | // Command Header & Footer | ||||||
|  | static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA}; | ||||||
|  | static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01}; | ||||||
|  | // Data Header & Footer | ||||||
|  | static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xAA, 0xFF, 0x03, 0x00}; | ||||||
|  | static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC}; | ||||||
|  | // MAC address the module uses when Bluetooth is disabled | ||||||
|  | static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; | ||||||
|  |  | ||||||
| static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; | static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; | ||||||
|  |  | ||||||
| static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) { | static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) { | ||||||
|   for (int i = 0; i < 4; i++) { |   for (uint8_t i = 0; i < 4; i++) { | ||||||
|     uint16_t val = values[i] & 0xFFFF; |     uint16_t val = values[i] & 0xFFFF; | ||||||
|     bytes[i * 2] = val & 0xFF;             // Store low byte first (little-endian) |     bytes[i * 2] = val & 0xFF;             // Store low byte first (little-endian) | ||||||
|     bytes[i * 2 + 1] = (val >> 8) & 0xFF;  // Store high byte second |     bytes[i * 2 + 1] = (val >> 8) & 0xFF;  // Store high byte second | ||||||
| @@ -166,18 +178,13 @@ static inline float calculate_angle(float base, float hypotenuse) { | |||||||
|   return angle_degrees; |   return angle_degrees; | ||||||
| } | } | ||||||
|  |  | ||||||
| static inline std::string get_direction(int16_t speed) { | static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { | ||||||
|   static const char *const APPROACHING = "Approaching"; |   for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) { | ||||||
|   static const char *const MOVING_AWAY = "Moving away"; |     if (header_footer[i] != buffer[i]) { | ||||||
|   static const char *const STATIONARY = "Stationary"; |       return false;  // Mismatch in header/footer | ||||||
|  |     } | ||||||
|   if (speed > 0) { |  | ||||||
|     return MOVING_AWAY; |  | ||||||
|   } |   } | ||||||
|   if (speed < 0) { |   return true;  // Valid header/footer | ||||||
|     return APPROACHING; |  | ||||||
|   } |  | ||||||
|   return STATIONARY; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2450Component::setup() { | void LD2450Component::setup() { | ||||||
| @@ -192,84 +199,93 @@ void LD2450Component::setup() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void LD2450Component::dump_config() { | void LD2450Component::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "LD2450:"); |   std::string mac_str = | ||||||
|  |       mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; | ||||||
|  |   std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], | ||||||
|  |                                     this->version_[4], this->version_[3], this->version_[2]); | ||||||
|  |   ESP_LOGCONFIG(TAG, | ||||||
|  |                 "LD2450:\n" | ||||||
|  |                 "  Firmware version: %s\n" | ||||||
|  |                 "  MAC address: %s\n" | ||||||
|  |                 "  Throttle: %u ms", | ||||||
|  |                 version.c_str(), mac_str.c_str(), this->throttle_); | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
|   LOG_BINARY_SENSOR("  ", "TargetBinarySensor", this->target_binary_sensor_); |   ESP_LOGCONFIG(TAG, "Binary Sensors:"); | ||||||
|   LOG_BINARY_SENSOR("  ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); |   LOG_BINARY_SENSOR("  ", "MovingTarget", this->moving_target_binary_sensor_); | ||||||
|   LOG_BINARY_SENSOR("  ", "StillTargetBinarySensor", this->still_target_binary_sensor_); |   LOG_BINARY_SENSOR("  ", "StillTarget", this->still_target_binary_sensor_); | ||||||
| #endif |   LOG_BINARY_SENSOR("  ", "Target", this->target_binary_sensor_); | ||||||
| #ifdef USE_SWITCH |  | ||||||
|   LOG_SWITCH("  ", "BluetoothSwitch", this->bluetooth_switch_); |  | ||||||
|   LOG_SWITCH("  ", "MultiTargetSwitch", this->multi_target_switch_); |  | ||||||
| #endif |  | ||||||
| #ifdef USE_BUTTON |  | ||||||
|   LOG_BUTTON("  ", "ResetButton", this->reset_button_); |  | ||||||
|   LOG_BUTTON("  ", "RestartButton", this->restart_button_); |  | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|   LOG_SENSOR("  ", "TargetCountSensor", this->target_count_sensor_); |   ESP_LOGCONFIG(TAG, "Sensors:"); | ||||||
|   LOG_SENSOR("  ", "StillTargetCountSensor", this->still_target_count_sensor_); |   LOG_SENSOR("  ", "MovingTargetCount", this->moving_target_count_sensor_); | ||||||
|   LOG_SENSOR("  ", "MovingTargetCountSensor", this->moving_target_count_sensor_); |   LOG_SENSOR("  ", "StillTargetCount", this->still_target_count_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "TargetCount", this->target_count_sensor_); | ||||||
|   for (sensor::Sensor *s : this->move_x_sensors_) { |   for (sensor::Sensor *s : this->move_x_sensors_) { | ||||||
|     LOG_SENSOR("  ", "NthTargetXSensor", s); |     LOG_SENSOR("  ", "TargetX", s); | ||||||
|   } |   } | ||||||
|   for (sensor::Sensor *s : this->move_y_sensors_) { |   for (sensor::Sensor *s : this->move_y_sensors_) { | ||||||
|     LOG_SENSOR("  ", "NthTargetYSensor", s); |     LOG_SENSOR("  ", "TargetY", s); | ||||||
|   } |  | ||||||
|   for (sensor::Sensor *s : this->move_speed_sensors_) { |  | ||||||
|     LOG_SENSOR("  ", "NthTargetSpeedSensor", s); |  | ||||||
|   } |   } | ||||||
|   for (sensor::Sensor *s : this->move_angle_sensors_) { |   for (sensor::Sensor *s : this->move_angle_sensors_) { | ||||||
|     LOG_SENSOR("  ", "NthTargetAngleSensor", s); |     LOG_SENSOR("  ", "TargetAngle", s); | ||||||
|   } |   } | ||||||
|   for (sensor::Sensor *s : this->move_distance_sensors_) { |   for (sensor::Sensor *s : this->move_distance_sensors_) { | ||||||
|     LOG_SENSOR("  ", "NthTargetDistanceSensor", s); |     LOG_SENSOR("  ", "TargetDistance", s); | ||||||
|   } |   } | ||||||
|   for (sensor::Sensor *s : this->move_resolution_sensors_) { |   for (sensor::Sensor *s : this->move_resolution_sensors_) { | ||||||
|     LOG_SENSOR("  ", "NthTargetResolutionSensor", s); |     LOG_SENSOR("  ", "TargetResolution", s); | ||||||
|  |   } | ||||||
|  |   for (sensor::Sensor *s : this->move_speed_sensors_) { | ||||||
|  |     LOG_SENSOR("  ", "TargetSpeed", s); | ||||||
|   } |   } | ||||||
|   for (sensor::Sensor *s : this->zone_target_count_sensors_) { |   for (sensor::Sensor *s : this->zone_target_count_sensors_) { | ||||||
|     LOG_SENSOR("  ", "NthZoneTargetCountSensor", s); |     LOG_SENSOR("  ", "ZoneTargetCount", s); | ||||||
|   } |  | ||||||
|   for (sensor::Sensor *s : this->zone_still_target_count_sensors_) { |  | ||||||
|     LOG_SENSOR("  ", "NthZoneStillTargetCountSensor", s); |  | ||||||
|   } |   } | ||||||
|   for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) { |   for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) { | ||||||
|     LOG_SENSOR("  ", "NthZoneMovingTargetCountSensor", s); |     LOG_SENSOR("  ", "ZoneMovingTargetCount", s); | ||||||
|  |   } | ||||||
|  |   for (sensor::Sensor *s : this->zone_still_target_count_sensors_) { | ||||||
|  |     LOG_SENSOR("  ", "ZoneStillTargetCount", s); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|   LOG_TEXT_SENSOR("  ", "VersionTextSensor", this->version_text_sensor_); |   ESP_LOGCONFIG(TAG, "Text Sensors:"); | ||||||
|   LOG_TEXT_SENSOR("  ", "MacTextSensor", this->mac_text_sensor_); |   LOG_TEXT_SENSOR("  ", "Version", this->version_text_sensor_); | ||||||
|  |   LOG_TEXT_SENSOR("  ", "Mac", this->mac_text_sensor_); | ||||||
|   for (text_sensor::TextSensor *s : this->direction_text_sensors_) { |   for (text_sensor::TextSensor *s : this->direction_text_sensors_) { | ||||||
|     LOG_TEXT_SENSOR("  ", "NthDirectionTextSensor", s); |     LOG_TEXT_SENSOR("  ", "Direction", s); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|  |   ESP_LOGCONFIG(TAG, "Numbers:"); | ||||||
|  |   LOG_NUMBER("  ", "PresenceTimeout", this->presence_timeout_number_); | ||||||
|   for (auto n : this->zone_numbers_) { |   for (auto n : this->zone_numbers_) { | ||||||
|     LOG_NUMBER("  ", "ZoneX1Number", n.x1); |     LOG_NUMBER("  ", "ZoneX1", n.x1); | ||||||
|     LOG_NUMBER("  ", "ZoneY1Number", n.y1); |     LOG_NUMBER("  ", "ZoneY1", n.y1); | ||||||
|     LOG_NUMBER("  ", "ZoneX2Number", n.x2); |     LOG_NUMBER("  ", "ZoneX2", n.x2); | ||||||
|     LOG_NUMBER("  ", "ZoneY2Number", n.y2); |     LOG_NUMBER("  ", "ZoneY2", n.y2); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|   LOG_SELECT("  ", "BaudRateSelect", this->baud_rate_select_); |   ESP_LOGCONFIG(TAG, "Selects:"); | ||||||
|   LOG_SELECT("  ", "ZoneTypeSelect", this->zone_type_select_); |   LOG_SELECT("  ", "BaudRate", this->baud_rate_select_); | ||||||
|  |   LOG_SELECT("  ", "ZoneType", this->zone_type_select_); | ||||||
| #endif | #endif | ||||||
| #ifdef USE_NUMBER | #ifdef USE_SWITCH | ||||||
|   LOG_NUMBER("  ", "PresenceTimeoutNumber", this->presence_timeout_number_); |   ESP_LOGCONFIG(TAG, "Switches:"); | ||||||
|  |   LOG_SWITCH("  ", "Bluetooth", this->bluetooth_switch_); | ||||||
|  |   LOG_SWITCH("  ", "MultiTarget", this->multi_target_switch_); | ||||||
|  | #endif | ||||||
|  | #ifdef USE_BUTTON | ||||||
|  |   ESP_LOGCONFIG(TAG, "Buttons:"); | ||||||
|  |   LOG_BUTTON("  ", "FactoryReset", this->factory_reset_button_); | ||||||
|  |   LOG_BUTTON("  ", "Restart", this->restart_button_); | ||||||
| #endif | #endif | ||||||
|   ESP_LOGCONFIG(TAG, |  | ||||||
|                 "  Throttle: %ums\n" |  | ||||||
|                 "  MAC Address: %s\n" |  | ||||||
|                 "  Firmware version: %s", |  | ||||||
|                 this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2450Component::loop() { | void LD2450Component::loop() { | ||||||
|   while (this->available()) { |   while (this->available()) { | ||||||
|     this->readline_(read(), this->buffer_data_, MAX_LINE_LENGTH); |     this->readline_(this->read()); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -304,7 +320,7 @@ void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_ | |||||||
|   this->zone_type_ = zone_type; |   this->zone_type_ = zone_type; | ||||||
|   int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1, |   int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1, | ||||||
|                              zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2}; |                              zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2}; | ||||||
|   for (int i = 0; i < MAX_ZONES; i++) { |   for (uint8_t i = 0; i < MAX_ZONES; i++) { | ||||||
|     this->zone_config_[i].x1 = zone_parameters[i * 4]; |     this->zone_config_[i].x1 = zone_parameters[i * 4]; | ||||||
|     this->zone_config_[i].y1 = zone_parameters[i * 4 + 1]; |     this->zone_config_[i].y1 = zone_parameters[i * 4 + 1]; | ||||||
|     this->zone_config_[i].x2 = zone_parameters[i * 4 + 2]; |     this->zone_config_[i].x2 = zone_parameters[i * 4 + 2]; | ||||||
| @@ -318,15 +334,15 @@ void LD2450Component::send_set_zone_command_() { | |||||||
|   uint8_t cmd_value[26] = {}; |   uint8_t cmd_value[26] = {}; | ||||||
|   uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00}; |   uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00}; | ||||||
|   uint8_t area_config[24] = {}; |   uint8_t area_config[24] = {}; | ||||||
|   for (int i = 0; i < MAX_ZONES; i++) { |   for (uint8_t i = 0; i < MAX_ZONES; i++) { | ||||||
|     int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2, |     int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2, | ||||||
|                      this->zone_config_[i].y2}; |                      this->zone_config_[i].y2}; | ||||||
|     ld2450::convert_int_values_to_hex(values, area_config + (i * 8)); |     ld2450::convert_int_values_to_hex(values, area_config + (i * 8)); | ||||||
|   } |   } | ||||||
|   std::memcpy(cmd_value, zone_type_bytes, 2); |   std::memcpy(cmd_value, zone_type_bytes, sizeof(zone_type_bytes)); | ||||||
|   std::memcpy(cmd_value + 2, area_config, 24); |   std::memcpy(cmd_value + 2, area_config, sizeof(area_config)); | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   this->send_command_(CMD_SET_ZONE, cmd_value, 26); |   this->send_command_(CMD_SET_ZONE, cmd_value, sizeof(cmd_value)); | ||||||
|   this->set_config_mode_(false); |   this->set_config_mode_(false); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -342,14 +358,14 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Extract, store and publish zone details LD2450 buffer | // Extract, store and publish zone details LD2450 buffer | ||||||
| void LD2450Component::process_zone_(uint8_t *buffer) { | void LD2450Component::process_zone_() { | ||||||
|   uint8_t index, start; |   uint8_t index, start; | ||||||
|   for (index = 0; index < MAX_ZONES; index++) { |   for (index = 0; index < MAX_ZONES; index++) { | ||||||
|     start = 12 + index * 8; |     start = 12 + index * 8; | ||||||
|     this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start); |     this->zone_config_[index].x1 = ld2450::hex_to_signed_int(this->buffer_data_, start); | ||||||
|     this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2); |     this->zone_config_[index].y1 = ld2450::hex_to_signed_int(this->buffer_data_, start + 2); | ||||||
|     this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4); |     this->zone_config_[index].x2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 4); | ||||||
|     this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6); |     this->zone_config_[index].y2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 6); | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|     // only one null check as all coordinates are required for a single zone |     // only one null check as all coordinates are required for a single zone | ||||||
|     if (this->zone_numbers_[index].x1 != nullptr) { |     if (this->zone_numbers_[index].x1 != nullptr) { | ||||||
| @@ -395,27 +411,25 @@ void LD2450Component::restart_and_read_all_info() { | |||||||
|  |  | ||||||
| // Send command with values to LD2450 | // Send command with values to LD2450 | ||||||
| void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { | void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { | ||||||
|   ESP_LOGV(TAG, "Sending command %02X", command); |   ESP_LOGV(TAG, "Sending COMMAND %02X", command); | ||||||
|   // frame header |   // frame header bytes | ||||||
|   this->write_array(CMD_FRAME_HEADER, 4); |   this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER)); | ||||||
|   // length bytes |   // length bytes | ||||||
|   int len = 2; |   uint8_t len = 2; | ||||||
|   if (command_value != nullptr) { |   if (command_value != nullptr) { | ||||||
|     len += command_value_len; |     len += command_value_len; | ||||||
|   } |   } | ||||||
|   this->write_byte(lowbyte(len)); |   uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00}; | ||||||
|   this->write_byte(highbyte(len)); |   this->write_array(len_cmd, sizeof(len_cmd)); | ||||||
|   // command |  | ||||||
|   this->write_byte(lowbyte(command)); |  | ||||||
|   this->write_byte(highbyte(command)); |  | ||||||
|   // command value bytes |   // command value bytes | ||||||
|   if (command_value != nullptr) { |   if (command_value != nullptr) { | ||||||
|     for (int i = 0; i < command_value_len; i++) { |     for (uint8_t i = 0; i < command_value_len; i++) { | ||||||
|       this->write_byte(command_value[i]); |       this->write_byte(command_value[i]); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   // footer |   // frame footer bytes | ||||||
|   this->write_array(CMD_FRAME_END, 4); |   this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); | ||||||
|   // FIXME to remove |   // FIXME to remove | ||||||
|   delay(50);  // NOLINT |   delay(50);  // NOLINT | ||||||
| } | } | ||||||
| @@ -423,26 +437,23 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu | |||||||
| // LD2450 Radar data message: | // LD2450 Radar data message: | ||||||
| //  [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC] | //  [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC] | ||||||
| //   Header       Target 1                  Target 2                  Target 3                  End | //   Header       Target 1                  Target 2                  Target 3                  End | ||||||
| void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | void LD2450Component::handle_periodic_data_() { | ||||||
|   // Early throttle check - moved before any processing to save CPU cycles |   // Early throttle check - moved before any processing to save CPU cycles | ||||||
|   if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { |   if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { | ||||||
|     ESP_LOGV(TAG, "Throttling: %d", this->throttle_); |  | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (len < 29) {  // header (4 bytes) + 8 x 3 target data + footer (2 bytes) |   if (this->buffer_pos_ < 29) {  // header (4 bytes) + 8 x 3 target data + footer (2 bytes) | ||||||
|     ESP_LOGE(TAG, "Invalid message length"); |     ESP_LOGE(TAG, "Invalid length"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) {  // header |   if (!ld2450::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || | ||||||
|     ESP_LOGE(TAG, "Invalid message header"); |       this->buffer_data_[this->buffer_pos_ - 2] != DATA_FRAME_FOOTER[0] || | ||||||
|  |       this->buffer_data_[this->buffer_pos_ - 1] != DATA_FRAME_FOOTER[1]) { | ||||||
|  |     ESP_LOGE(TAG, "Invalid header/footer"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) {  // footer |   // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately | ||||||
|     ESP_LOGE(TAG, "Invalid message footer"); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   this->last_periodic_millis_ = App.get_loop_component_start_time(); |   this->last_periodic_millis_ = App.get_loop_component_start_time(); | ||||||
|  |  | ||||||
|   int16_t target_count = 0; |   int16_t target_count = 0; | ||||||
| @@ -450,13 +461,13 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | |||||||
|   int16_t moving_target_count = 0; |   int16_t moving_target_count = 0; | ||||||
|   int16_t start = 0; |   int16_t start = 0; | ||||||
|   int16_t val = 0; |   int16_t val = 0; | ||||||
|   uint8_t index = 0; |  | ||||||
|   int16_t tx = 0; |   int16_t tx = 0; | ||||||
|   int16_t ty = 0; |   int16_t ty = 0; | ||||||
|   int16_t td = 0; |   int16_t td = 0; | ||||||
|   int16_t ts = 0; |   int16_t ts = 0; | ||||||
|   int16_t angle = 0; |   int16_t angle = 0; | ||||||
|   std::string direction{}; |   uint8_t index = 0; | ||||||
|  |   Direction direction{DIRECTION_UNDEFINED}; | ||||||
|   bool is_moving = false; |   bool is_moving = false; | ||||||
|  |  | ||||||
| #if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR) | #if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR) | ||||||
| @@ -468,7 +479,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | |||||||
|     is_moving = false; |     is_moving = false; | ||||||
|     sensor::Sensor *sx = this->move_x_sensors_[index]; |     sensor::Sensor *sx = this->move_x_sensors_[index]; | ||||||
|     if (sx != nullptr) { |     if (sx != nullptr) { | ||||||
|       val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]); |       val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); | ||||||
|       tx = val; |       tx = val; | ||||||
|       if (this->cached_target_data_[index].x != val) { |       if (this->cached_target_data_[index].x != val) { | ||||||
|         sx->publish_state(val); |         sx->publish_state(val); | ||||||
| @@ -479,7 +490,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | |||||||
|     start = TARGET_Y + index * 8; |     start = TARGET_Y + index * 8; | ||||||
|     sensor::Sensor *sy = this->move_y_sensors_[index]; |     sensor::Sensor *sy = this->move_y_sensors_[index]; | ||||||
|     if (sy != nullptr) { |     if (sy != nullptr) { | ||||||
|       val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]); |       val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); | ||||||
|       ty = val; |       ty = val; | ||||||
|       if (this->cached_target_data_[index].y != val) { |       if (this->cached_target_data_[index].y != val) { | ||||||
|         sy->publish_state(val); |         sy->publish_state(val); | ||||||
| @@ -490,7 +501,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | |||||||
|     start = TARGET_RESOLUTION + index * 8; |     start = TARGET_RESOLUTION + index * 8; | ||||||
|     sensor::Sensor *sr = this->move_resolution_sensors_[index]; |     sensor::Sensor *sr = this->move_resolution_sensors_[index]; | ||||||
|     if (sr != nullptr) { |     if (sr != nullptr) { | ||||||
|       val = (buffer[start + 1] << 8) | buffer[start]; |       val = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start]; | ||||||
|       if (this->cached_target_data_[index].resolution != val) { |       if (this->cached_target_data_[index].resolution != val) { | ||||||
|         sr->publish_state(val); |         sr->publish_state(val); | ||||||
|         this->cached_target_data_[index].resolution = val; |         this->cached_target_data_[index].resolution = val; | ||||||
| @@ -499,7 +510,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | |||||||
| #endif | #endif | ||||||
|     // SPEED |     // SPEED | ||||||
|     start = TARGET_SPEED + index * 8; |     start = TARGET_SPEED + index * 8; | ||||||
|     val = ld2450::decode_speed(buffer[start], buffer[start + 1]); |     val = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]); | ||||||
|     ts = val; |     ts = val; | ||||||
|     if (val) { |     if (val) { | ||||||
|       is_moving = true; |       is_moving = true; | ||||||
| @@ -532,7 +543,7 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     // ANGLE |     // ANGLE | ||||||
|     angle = calculate_angle(static_cast<float>(ty), static_cast<float>(td)); |     angle = ld2450::calculate_angle(static_cast<float>(ty), static_cast<float>(td)); | ||||||
|     if (tx > 0) { |     if (tx > 0) { | ||||||
|       angle = angle * -1; |       angle = angle * -1; | ||||||
|     } |     } | ||||||
| @@ -547,14 +558,19 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | |||||||
| #endif | #endif | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|     // DIRECTION |     // DIRECTION | ||||||
|     direction = get_direction(ts); |  | ||||||
|     if (td == 0) { |     if (td == 0) { | ||||||
|       direction = "NA"; |       direction = DIRECTION_NA; | ||||||
|  |     } else if (ts > 0) { | ||||||
|  |       direction = DIRECTION_MOVING_AWAY; | ||||||
|  |     } else if (ts < 0) { | ||||||
|  |       direction = DIRECTION_APPROACHING; | ||||||
|  |     } else { | ||||||
|  |       direction = DIRECTION_STATIONARY; | ||||||
|     } |     } | ||||||
|     text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; |     text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; | ||||||
|     if (tsd != nullptr) { |     if (tsd != nullptr) { | ||||||
|       if (this->cached_target_data_[index].direction != direction) { |       if (this->cached_target_data_[index].direction != direction) { | ||||||
|         tsd->publish_state(direction); |         tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction)); | ||||||
|         this->cached_target_data_[index].direction = direction; |         this->cached_target_data_[index].direction = direction; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -678,117 +694,139 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { | |||||||
| #endif | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
| bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | bool LD2450Component::handle_ack_data_() { | ||||||
|   ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]); |   ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]); | ||||||
|   if (len < 10) { |   if (this->buffer_pos_ < 10) { | ||||||
|     ESP_LOGE(TAG, "Invalid ack length"); |     ESP_LOGE(TAG, "Invalid length"); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|   if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) {  // frame header |   if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { | ||||||
|     ESP_LOGE(TAG, "Invalid ack header (command %02X)", buffer[COMMAND]); |     ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|   if (buffer[COMMAND_STATUS] != 0x01) { |   if (this->buffer_data_[COMMAND_STATUS] != 0x01) { | ||||||
|     ESP_LOGE(TAG, "Invalid ack status"); |     ESP_LOGE(TAG, "Invalid status"); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|   if (buffer[8] || buffer[9]) { |   if (this->buffer_data_[8] || this->buffer_data_[9]) { | ||||||
|     ESP_LOGE(TAG, "Last buffer was %u, %u", buffer[8], buffer[9]); |     ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   switch (buffer[COMMAND]) { |   switch (this->buffer_data_[COMMAND]) { | ||||||
|     case lowbyte(CMD_ENABLE_CONF): |     case CMD_ENABLE_CONF: | ||||||
|       ESP_LOGV(TAG, "Enable conf command"); |       ESP_LOGV(TAG, "Enable conf"); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_DISABLE_CONF): |  | ||||||
|       ESP_LOGV(TAG, "Disable conf command"); |     case CMD_DISABLE_CONF: | ||||||
|  |       ESP_LOGV(TAG, "Disabled conf"); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_SET_BAUD_RATE): |  | ||||||
|       ESP_LOGV(TAG, "Baud rate change command"); |     case CMD_SET_BAUD_RATE: | ||||||
|  |       ESP_LOGV(TAG, "Baud rate change"); | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|       if (this->baud_rate_select_ != nullptr) { |       if (this->baud_rate_select_ != nullptr) { | ||||||
|         ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str()); |         ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_VERSION): |  | ||||||
|       this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); |     case CMD_QUERY_VERSION: { | ||||||
|       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); |       std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); | ||||||
|  |       std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], | ||||||
|  |                                         this->version_[4], this->version_[3], this->version_[2]); | ||||||
|  |       ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|       if (this->version_text_sensor_ != nullptr) { |       if (this->version_text_sensor_ != nullptr) { | ||||||
|         this->version_text_sensor_->publish_state(this->version_); |         this->version_text_sensor_->publish_state(version); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_MAC): |     } | ||||||
|       if (len < 20) { |  | ||||||
|  |     case CMD_QUERY_MAC_ADDRESS: { | ||||||
|  |       if (this->buffer_pos_ < 20) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|       this->mac_ = format_mac_address_pretty(&buffer[10]); |  | ||||||
|       ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); |       this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0; | ||||||
|  |       if (this->bluetooth_on_) { | ||||||
|  |         std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       std::string mac_str = | ||||||
|  |           mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; | ||||||
|  |       ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|       if (this->mac_text_sensor_ != nullptr) { |       if (this->mac_text_sensor_ != nullptr) { | ||||||
|         this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_); |         this->mac_text_sensor_->publish_state(mac_str); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|       if (this->bluetooth_switch_ != nullptr) { |       if (this->bluetooth_switch_ != nullptr) { | ||||||
|         this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); |         this->bluetooth_switch_->publish_state(this->bluetooth_on_); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_BLUETOOTH): |     } | ||||||
|       ESP_LOGV(TAG, "Bluetooth command"); |  | ||||||
|  |     case CMD_BLUETOOTH: | ||||||
|  |       ESP_LOGV(TAG, "Bluetooth"); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_SINGLE_TARGET_MODE): |  | ||||||
|       ESP_LOGV(TAG, "Single target conf command"); |     case CMD_SINGLE_TARGET_MODE: | ||||||
|  |       ESP_LOGV(TAG, "Single target conf"); | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|       if (this->multi_target_switch_ != nullptr) { |       if (this->multi_target_switch_ != nullptr) { | ||||||
|         this->multi_target_switch_->publish_state(false); |         this->multi_target_switch_->publish_state(false); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_MULTI_TARGET_MODE): |  | ||||||
|       ESP_LOGV(TAG, "Multi target conf command"); |     case CMD_MULTI_TARGET_MODE: | ||||||
|  |       ESP_LOGV(TAG, "Multi target conf"); | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|       if (this->multi_target_switch_ != nullptr) { |       if (this->multi_target_switch_ != nullptr) { | ||||||
|         this->multi_target_switch_->publish_state(true); |         this->multi_target_switch_->publish_state(true); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_QUERY_TARGET_MODE): |  | ||||||
|       ESP_LOGV(TAG, "Query target tracking mode command"); |     case CMD_QUERY_TARGET_MODE: | ||||||
|  |       ESP_LOGV(TAG, "Query target tracking mode"); | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|       if (this->multi_target_switch_ != nullptr) { |       if (this->multi_target_switch_ != nullptr) { | ||||||
|         this->multi_target_switch_->publish_state(buffer[10] == 0x02); |         this->multi_target_switch_->publish_state(this->buffer_data_[10] == 0x02); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_QUERY_ZONE): |  | ||||||
|       ESP_LOGV(TAG, "Query zone conf command"); |     case CMD_QUERY_ZONE: | ||||||
|       this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16); |       ESP_LOGV(TAG, "Query zone conf"); | ||||||
|  |       this->zone_type_ = std::stoi(std::to_string(this->buffer_data_[10]), nullptr, 16); | ||||||
|       this->publish_zone_type(); |       this->publish_zone_type(); | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|       if (this->zone_type_select_ != nullptr) { |       if (this->zone_type_select_ != nullptr) { | ||||||
|         ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); |         ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       if (buffer[10] == 0x00) { |       if (this->buffer_data_[10] == 0x00) { | ||||||
|         ESP_LOGV(TAG, "Zone: Disabled"); |         ESP_LOGV(TAG, "Zone: Disabled"); | ||||||
|       } |       } | ||||||
|       if (buffer[10] == 0x01) { |       if (this->buffer_data_[10] == 0x01) { | ||||||
|         ESP_LOGV(TAG, "Zone: Area detection"); |         ESP_LOGV(TAG, "Zone: Area detection"); | ||||||
|       } |       } | ||||||
|       if (buffer[10] == 0x02) { |       if (this->buffer_data_[10] == 0x02) { | ||||||
|         ESP_LOGV(TAG, "Zone: Area filter"); |         ESP_LOGV(TAG, "Zone: Area filter"); | ||||||
|       } |       } | ||||||
|       this->process_zone_(buffer); |       this->process_zone_(); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_SET_ZONE): |  | ||||||
|       ESP_LOGV(TAG, "Set zone conf command"); |     case CMD_SET_ZONE: | ||||||
|  |       ESP_LOGV(TAG, "Set zone conf"); | ||||||
|       this->query_zone_info(); |       this->query_zone_info(); | ||||||
|       break; |       break; | ||||||
|  |  | ||||||
|     default: |     default: | ||||||
|       break; |       break; | ||||||
|   } |   } | ||||||
| @@ -796,55 +834,57 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Read LD2450 buffer data | // Read LD2450 buffer data | ||||||
| void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) { | void LD2450Component::readline_(int readch) { | ||||||
|   if (readch < 0) { |   if (readch < 0) { | ||||||
|     return; |     return;  // No data available | ||||||
|   } |   } | ||||||
|   if (this->buffer_pos_ < len - 1) { |  | ||||||
|     buffer[this->buffer_pos_++] = readch; |   if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { | ||||||
|     buffer[this->buffer_pos_] = 0; |     this->buffer_data_[this->buffer_pos_++] = readch; | ||||||
|  |     this->buffer_data_[this->buffer_pos_] = 0; | ||||||
|   } else { |   } else { | ||||||
|  |     // We should never get here, but just in case... | ||||||
|  |     ESP_LOGW(TAG, "Max command length exceeded; ignoring"); | ||||||
|     this->buffer_pos_ = 0; |     this->buffer_pos_ = 0; | ||||||
|   } |   } | ||||||
|   if (this->buffer_pos_ < 4) { |   if (this->buffer_pos_ < 4) { | ||||||
|     return; |     return;  // Not enough data to process yet | ||||||
|   } |   } | ||||||
|   if (buffer[this->buffer_pos_ - 2] == 0x55 && buffer[this->buffer_pos_ - 1] == 0xCC) { |   if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && | ||||||
|     ESP_LOGV(TAG, "Handle periodic radar data"); |       this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) { | ||||||
|     this->handle_periodic_data_(buffer, this->buffer_pos_); |     ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); | ||||||
|  |     this->handle_periodic_data_(); | ||||||
|     this->buffer_pos_ = 0;  // Reset position index for next frame |     this->buffer_pos_ = 0;  // Reset position index for next frame | ||||||
|   } else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 && |   } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { | ||||||
|              buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) { |     ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); | ||||||
|     ESP_LOGV(TAG, "Handle command ack data"); |     if (this->handle_ack_data_()) { | ||||||
|     if (this->handle_ack_data_(buffer, this->buffer_pos_)) { |       this->buffer_pos_ = 0;  // Reset position index for next message | ||||||
|       this->buffer_pos_ = 0;  // Reset position index for next frame |  | ||||||
|     } else { |     } else { | ||||||
|       ESP_LOGV(TAG, "Command ack data invalid"); |       ESP_LOGV(TAG, "Ack Data incomplete"); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| // Set Config Mode - Pre-requisite sending commands | // Set Config Mode - Pre-requisite sending commands | ||||||
| void LD2450Component::set_config_mode_(bool enable) { | void LD2450Component::set_config_mode_(bool enable) { | ||||||
|   uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; |   const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; | ||||||
|   uint8_t cmd_value[2] = {0x01, 0x00}; |   const uint8_t cmd_value[2] = {0x01, 0x00}; | ||||||
|   this->send_command_(cmd, enable ? cmd_value : nullptr, 2); |   this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value)); | ||||||
| } | } | ||||||
|  |  | ||||||
| // Set Bluetooth Enable/Disable | // Set Bluetooth Enable/Disable | ||||||
| void LD2450Component::set_bluetooth(bool enable) { | void LD2450Component::set_bluetooth(bool enable) { | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t enable_cmd_value[2] = {0x01, 0x00}; |   const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00}; | ||||||
|   uint8_t disable_cmd_value[2] = {0x00, 0x00}; |   this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value)); | ||||||
|   this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2); |  | ||||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); |   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||||
| } | } | ||||||
|  |  | ||||||
| // Set Baud rate | // Set Baud rate | ||||||
| void LD2450Component::set_baud_rate(const std::string &state) { | void LD2450Component::set_baud_rate(const std::string &state) { | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; |   const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; | ||||||
|   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); |   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); | ||||||
|   this->set_timeout(200, [this]() { this->restart_(); }); |   this->set_timeout(200, [this]() { this->restart_(); }); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -885,12 +925,12 @@ void LD2450Component::factory_reset() { | |||||||
| void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } | void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } | ||||||
|  |  | ||||||
| // Get LD2450 firmware version | // Get LD2450 firmware version | ||||||
| void LD2450Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); } | void LD2450Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); } | ||||||
|  |  | ||||||
| // Get LD2450 mac address | // Get LD2450 mac address | ||||||
| void LD2450Component::get_mac_() { | void LD2450Component::get_mac_() { | ||||||
|   uint8_t cmd_value[2] = {0x01, 0x00}; |   uint8_t cmd_value[2] = {0x01, 0x00}; | ||||||
|   this->send_command_(CMD_MAC, cmd_value, 2); |   this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, 2); | ||||||
| } | } | ||||||
|  |  | ||||||
| // Query for target tracking mode | // Query for target tracking mode | ||||||
|   | |||||||
| @@ -38,10 +38,18 @@ namespace ld2450 { | |||||||
|  |  | ||||||
| // Constants | // Constants | ||||||
| static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5;  // Timeout to reset presense status 5 sec. | static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5;  // Timeout to reset presense status 5 sec. | ||||||
| static const uint8_t MAX_LINE_LENGTH = 60;          // Max characters for serial buffer | static const uint8_t MAX_LINE_LENGTH = 41;          // Max characters for serial buffer | ||||||
| static const uint8_t MAX_TARGETS = 3;               // Max 3 Targets in LD2450 | static const uint8_t MAX_TARGETS = 3;               // Max 3 Targets in LD2450 | ||||||
| static const uint8_t MAX_ZONES = 3;                 // Max 3 Zones in LD2450 | static const uint8_t MAX_ZONES = 3;                 // Max 3 Zones in LD2450 | ||||||
|  |  | ||||||
|  | enum Direction : uint8_t { | ||||||
|  |   DIRECTION_APPROACHING = 0, | ||||||
|  |   DIRECTION_MOVING_AWAY = 1, | ||||||
|  |   DIRECTION_STATIONARY = 2, | ||||||
|  |   DIRECTION_NA = 3, | ||||||
|  |   DIRECTION_UNDEFINED = 4, | ||||||
|  | }; | ||||||
|  |  | ||||||
| // Target coordinate struct | // Target coordinate struct | ||||||
| struct Target { | struct Target { | ||||||
|   int16_t x; |   int16_t x; | ||||||
| @@ -67,19 +75,22 @@ struct ZoneOfNumbers { | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
| class LD2450Component : public Component, public uart::UARTDevice { | class LD2450Component : public Component, public uart::UARTDevice { | ||||||
| #ifdef USE_SENSOR |  | ||||||
|   SUB_SENSOR(target_count) |  | ||||||
|   SUB_SENSOR(still_target_count) |  | ||||||
|   SUB_SENSOR(moving_target_count) |  | ||||||
| #endif |  | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
|   SUB_BINARY_SENSOR(target) |  | ||||||
|   SUB_BINARY_SENSOR(moving_target) |   SUB_BINARY_SENSOR(moving_target) | ||||||
|   SUB_BINARY_SENSOR(still_target) |   SUB_BINARY_SENSOR(still_target) | ||||||
|  |   SUB_BINARY_SENSOR(target) | ||||||
|  | #endif | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  |   SUB_SENSOR(moving_target_count) | ||||||
|  |   SUB_SENSOR(still_target_count) | ||||||
|  |   SUB_SENSOR(target_count) | ||||||
| #endif | #endif | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|   SUB_TEXT_SENSOR(version) |  | ||||||
|   SUB_TEXT_SENSOR(mac) |   SUB_TEXT_SENSOR(mac) | ||||||
|  |   SUB_TEXT_SENSOR(version) | ||||||
|  | #endif | ||||||
|  | #ifdef USE_NUMBER | ||||||
|  |   SUB_NUMBER(presence_timeout) | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|   SUB_SELECT(baud_rate) |   SUB_SELECT(baud_rate) | ||||||
| @@ -90,12 +101,9 @@ class LD2450Component : public Component, public uart::UARTDevice { | |||||||
|   SUB_SWITCH(multi_target) |   SUB_SWITCH(multi_target) | ||||||
| #endif | #endif | ||||||
| #ifdef USE_BUTTON | #ifdef USE_BUTTON | ||||||
|   SUB_BUTTON(reset) |   SUB_BUTTON(factory_reset) | ||||||
|   SUB_BUTTON(restart) |   SUB_BUTTON(restart) | ||||||
| #endif | #endif | ||||||
| #ifdef USE_NUMBER |  | ||||||
|   SUB_NUMBER(presence_timeout) |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
|  public: |  public: | ||||||
|   void setup() override; |   void setup() override; | ||||||
| @@ -138,10 +146,10 @@ class LD2450Component : public Component, public uart::UARTDevice { | |||||||
|  protected: |  protected: | ||||||
|   void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); |   void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); | ||||||
|   void set_config_mode_(bool enable); |   void set_config_mode_(bool enable); | ||||||
|   void handle_periodic_data_(uint8_t *buffer, uint8_t len); |   void handle_periodic_data_(); | ||||||
|   bool handle_ack_data_(uint8_t *buffer, uint8_t len); |   bool handle_ack_data_(); | ||||||
|   void process_zone_(uint8_t *buffer); |   void process_zone_(); | ||||||
|   void readline_(int readch, uint8_t *buffer, uint8_t len); |   void readline_(int readch); | ||||||
|   void get_version_(); |   void get_version_(); | ||||||
|   void get_mac_(); |   void get_mac_(); | ||||||
|   void query_target_tracking_mode_(); |   void query_target_tracking_mode_(); | ||||||
| @@ -159,13 +167,14 @@ class LD2450Component : public Component, public uart::UARTDevice { | |||||||
|   uint32_t moving_presence_millis_ = 0; |   uint32_t moving_presence_millis_ = 0; | ||||||
|   uint16_t throttle_ = 0; |   uint16_t throttle_ = 0; | ||||||
|   uint16_t timeout_ = 5; |   uint16_t timeout_ = 5; | ||||||
|   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer |  | ||||||
|   uint8_t buffer_data_[MAX_LINE_LENGTH]; |   uint8_t buffer_data_[MAX_LINE_LENGTH]; | ||||||
|  |   uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; | ||||||
|  |   uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; | ||||||
|  |   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer | ||||||
|   uint8_t zone_type_ = 0; |   uint8_t zone_type_ = 0; | ||||||
|  |   bool bluetooth_on_{false}; | ||||||
|   Target target_info_[MAX_TARGETS]; |   Target target_info_[MAX_TARGETS]; | ||||||
|   Zone zone_config_[MAX_ZONES]; |   Zone zone_config_[MAX_ZONES]; | ||||||
|   std::string version_{}; |  | ||||||
|   std::string mac_{}; |  | ||||||
|  |  | ||||||
|   // Change detection - cache previous values to avoid redundant publishes |   // Change detection - cache previous values to avoid redundant publishes | ||||||
|   // All values are initialized to sentinel values that are outside the valid sensor ranges |   // All values are initialized to sentinel values that are outside the valid sensor ranges | ||||||
| @@ -176,8 +185,8 @@ class LD2450Component : public Component, public uart::UARTDevice { | |||||||
|     int16_t speed = std::numeric_limits<int16_t>::min();         // -32768, outside practical sensor range |     int16_t speed = std::numeric_limits<int16_t>::min();         // -32768, outside practical sensor range | ||||||
|     uint16_t resolution = std::numeric_limits<uint16_t>::max();  // 65535, unlikely resolution value |     uint16_t resolution = std::numeric_limits<uint16_t>::max();  // 65535, unlikely resolution value | ||||||
|     uint16_t distance = std::numeric_limits<uint16_t>::max();    // 65535, outside range of 0 to ~8990 |     uint16_t distance = std::numeric_limits<uint16_t>::max();    // 65535, outside range of 0 to ~8990 | ||||||
|  |     Direction direction = DIRECTION_UNDEFINED;                   // Undefined, will differ from any real direction | ||||||
|     float angle = NAN;                                           // NAN, safe sentinel for floats |     float angle = NAN;                                           // NAN, safe sentinel for floats | ||||||
|     std::string direction = "";                                  // Empty string, will differ from any real direction |  | ||||||
|   } cached_target_data_[MAX_TARGETS]; |   } cached_target_data_[MAX_TARGETS]; | ||||||
|  |  | ||||||
|   struct CachedZoneData { |   struct CachedZoneData { | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								esphome/components/libretiny/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								esphome/components/libretiny/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_LIBRETINY | ||||||
|  |  | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
|  | #include <WiFi.h>  // for macAddress() | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  |  | ||||||
|  | uint32_t random_uint32() { return rand(); } | ||||||
|  |  | ||||||
|  | bool random_bytes(uint8_t *data, size_t len) { | ||||||
|  |   lt_rand_bytes(data, len); | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } | ||||||
|  | Mutex::~Mutex() {} | ||||||
|  | void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } | ||||||
|  | bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } | ||||||
|  | void Mutex::unlock() { xSemaphoreGive(this->handle_); } | ||||||
|  |  | ||||||
|  | // only affects the executing core | ||||||
|  | // so should not be used as a mutex lock, only to get accurate timing | ||||||
|  | IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } | ||||||
|  | IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } | ||||||
|  |  | ||||||
|  | void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) | ||||||
|  |   WiFi.macAddress(mac); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // USE_LIBRETINY | ||||||
| @@ -11,6 +11,7 @@ CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" | |||||||
| CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" | CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" | ||||||
| CONF_COMMAND_SPACING = "command_spacing" | CONF_COMMAND_SPACING = "command_spacing" | ||||||
| CONF_COMPONENT_NAME = "component_name" | CONF_COMPONENT_NAME = "component_name" | ||||||
|  | CONF_DUMP_DEVICE_INFO = "dump_device_info" | ||||||
| CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" | CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" | ||||||
| CONF_FONT_ID = "font_id" | CONF_FONT_ID = "font_id" | ||||||
| CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" | CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ from . import Nextion, nextion_ns, nextion_ref | |||||||
| from .base_component import ( | from .base_component import ( | ||||||
|     CONF_AUTO_WAKE_ON_TOUCH, |     CONF_AUTO_WAKE_ON_TOUCH, | ||||||
|     CONF_COMMAND_SPACING, |     CONF_COMMAND_SPACING, | ||||||
|  |     CONF_DUMP_DEVICE_INFO, | ||||||
|     CONF_EXIT_REPARSE_ON_START, |     CONF_EXIT_REPARSE_ON_START, | ||||||
|     CONF_MAX_COMMANDS_PER_LOOP, |     CONF_MAX_COMMANDS_PER_LOOP, | ||||||
|     CONF_MAX_QUEUE_SIZE, |     CONF_MAX_QUEUE_SIZE, | ||||||
| @@ -57,6 +58,7 @@ CONFIG_SCHEMA = ( | |||||||
|                 cv.positive_time_period_milliseconds, |                 cv.positive_time_period_milliseconds, | ||||||
|                 cv.Range(max=TimePeriod(milliseconds=255)), |                 cv.Range(max=TimePeriod(milliseconds=255)), | ||||||
|             ), |             ), | ||||||
|  |             cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean, | ||||||
|             cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, |             cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, | ||||||
|             cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t, |             cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t, | ||||||
|             cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int, |             cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int, | ||||||
| @@ -95,7 +97,9 @@ CONFIG_SCHEMA = ( | |||||||
|             cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean, |             cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean, | ||||||
|             cv.Optional(CONF_START_UP_PAGE): cv.uint8_t, |             cv.Optional(CONF_START_UP_PAGE): cv.uint8_t, | ||||||
|             cv.Optional(CONF_TFT_URL): cv.url, |             cv.Optional(CONF_TFT_URL): cv.url, | ||||||
|             cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535), |             cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any( | ||||||
|  |                 0, cv.int_range(min=3, max=65535) | ||||||
|  |             ), | ||||||
|             cv.Optional(CONF_WAKE_UP_PAGE): cv.uint8_t, |             cv.Optional(CONF_WAKE_UP_PAGE): cv.uint8_t, | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
| @@ -172,9 +176,14 @@ async def to_code(config): | |||||||
|  |  | ||||||
|     cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH])) |     cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH])) | ||||||
|  |  | ||||||
|     cg.add(var.set_exit_reparse_on_start(config[CONF_EXIT_REPARSE_ON_START])) |     if config[CONF_DUMP_DEVICE_INFO]: | ||||||
|  |         cg.add_define("USE_NEXTION_CONFIG_DUMP_DEVICE_INFO") | ||||||
|  |  | ||||||
|     cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE])) |     if config[CONF_EXIT_REPARSE_ON_START]: | ||||||
|  |         cg.add_define("USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START") | ||||||
|  |  | ||||||
|  |     if config[CONF_SKIP_CONNECTION_HANDSHAKE]: | ||||||
|  |         cg.add_define("USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE") | ||||||
|  |  | ||||||
|     if max_commands_per_loop := config.get(CONF_MAX_COMMANDS_PER_LOOP): |     if max_commands_per_loop := config.get(CONF_MAX_COMMANDS_PER_LOOP): | ||||||
|         cg.add_define("USE_NEXTION_MAX_COMMANDS_PER_LOOP") |         cg.add_define("USE_NEXTION_MAX_COMMANDS_PER_LOOP") | ||||||
|   | |||||||
| @@ -13,14 +13,11 @@ void Nextion::setup() { | |||||||
|   this->is_setup_ = false; |   this->is_setup_ = false; | ||||||
|   this->connection_state_.ignore_is_setup_ = true; |   this->connection_state_.ignore_is_setup_ = true; | ||||||
|  |  | ||||||
|   // Wake up the nextion |   // Wake up the nextion and ensure clean communication state | ||||||
|   this->send_command_("bkcmd=0"); |   this->send_command_("sleep=0");  // Exit sleep mode if sleeping | ||||||
|   this->send_command_("sleep=0"); |   this->send_command_("bkcmd=0");  // Disable return data during init sequence | ||||||
|  |  | ||||||
|   this->send_command_("bkcmd=0"); |   // Reset device for clean state - critical for reliable communication | ||||||
|   this->send_command_("sleep=0"); |  | ||||||
|  |  | ||||||
|   // Reboot it |  | ||||||
|   this->send_command_("rest"); |   this->send_command_("rest"); | ||||||
|  |  | ||||||
|   this->connection_state_.ignore_is_setup_ = false; |   this->connection_state_.ignore_is_setup_ = false; | ||||||
| @@ -51,24 +48,19 @@ bool Nextion::check_connect_() { | |||||||
|   if (this->connection_state_.is_connected_) |   if (this->connection_state_.is_connected_) | ||||||
|     return true; |     return true; | ||||||
|  |  | ||||||
|   // Check if the handshake should be skipped for the Nextion connection | #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE | ||||||
|   if (this->skip_connection_handshake_) { |   ESP_LOGW(TAG, "Connected (no handshake)");  // Log the connection status without handshake | ||||||
|     // Log the connection status without handshake |   this->is_connected_ = true;                 // Set the connection status to true | ||||||
|     ESP_LOGW(TAG, "Connected (no handshake)"); |   return true;                                // Return true indicating the connection is set | ||||||
|     // Set the connection status to true | #else                                         // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE | ||||||
|     this->connection_state_.is_connected_ = true; |  | ||||||
|     // Return true indicating the connection is set |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (this->comok_sent_ == 0) { |   if (this->comok_sent_ == 0) { | ||||||
|     this->reset_(false); |     this->reset_(false); | ||||||
|  |  | ||||||
|     this->connection_state_.ignore_is_setup_ = true; |     this->connection_state_.ignore_is_setup_ = true; | ||||||
|     this->send_command_("boguscommand=0");  // bogus command. needed sometimes after updating |     this->send_command_("boguscommand=0");  // bogus command. needed sometimes after updating | ||||||
|     if (this->exit_reparse_on_start_) { | #ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START | ||||||
|       this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); |     this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); | ||||||
|     } | #endif  // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START | ||||||
|     this->send_command_("connect"); |     this->send_command_("connect"); | ||||||
|  |  | ||||||
|     this->comok_sent_ = App.get_loop_component_start_time(); |     this->comok_sent_ = App.get_loop_component_start_time(); | ||||||
| @@ -94,7 +86,7 @@ bool Nextion::check_connect_() { | |||||||
|     for (size_t i = 0; i < response.length(); i++) { |     for (size_t i = 0; i < response.length(); i++) { | ||||||
|       ESP_LOGN(TAG, "resp: %s %d %d %c", response.c_str(), i, response[i], response[i]); |       ESP_LOGN(TAG, "resp: %s %d %d %c", response.c_str(), i, response[i], response[i]); | ||||||
|     } |     } | ||||||
| #endif | #endif  // NEXTION_PROTOCOL_LOG | ||||||
|  |  | ||||||
|     ESP_LOGW(TAG, "Not connected"); |     ESP_LOGW(TAG, "Not connected"); | ||||||
|     comok_sent_ = 0; |     comok_sent_ = 0; | ||||||
| @@ -118,11 +110,19 @@ bool Nextion::check_connect_() { | |||||||
|   this->is_detected_ = (connect_info.size() == 7); |   this->is_detected_ = (connect_info.size() == 7); | ||||||
|   if (this->is_detected_) { |   if (this->is_detected_) { | ||||||
|     ESP_LOGN(TAG, "Connect info: %zu", connect_info.size()); |     ESP_LOGN(TAG, "Connect info: %zu", connect_info.size()); | ||||||
|  | #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO | ||||||
|     this->device_model_ = connect_info[2]; |     this->device_model_ = connect_info[2]; | ||||||
|     this->firmware_version_ = connect_info[3]; |     this->firmware_version_ = connect_info[3]; | ||||||
|     this->serial_number_ = connect_info[5]; |     this->serial_number_ = connect_info[5]; | ||||||
|     this->flash_size_ = connect_info[6]; |     this->flash_size_ = connect_info[6]; | ||||||
|  | #else   // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO | ||||||
|  |     ESP_LOGI(TAG, | ||||||
|  |              "  Device Model:   %s\n" | ||||||
|  |              "  FW Version:     %s\n" | ||||||
|  |              "  Serial Number:  %s\n" | ||||||
|  |              "  Flash Size:     %s\n", | ||||||
|  |              connect_info[2].c_str(), connect_info[3].c_str(), connect_info[5].c_str(), connect_info[6].c_str()); | ||||||
|  | #endif  // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO | ||||||
|   } else { |   } else { | ||||||
|     ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str()); |     ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str()); | ||||||
|   } |   } | ||||||
| @@ -130,6 +130,7 @@ bool Nextion::check_connect_() { | |||||||
|   this->connection_state_.ignore_is_setup_ = false; |   this->connection_state_.ignore_is_setup_ = false; | ||||||
|   this->dump_config(); |   this->dump_config(); | ||||||
|   return true; |   return true; | ||||||
|  | #endif  // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE | ||||||
| } | } | ||||||
|  |  | ||||||
| void Nextion::reset_(bool reset_nextion) { | void Nextion::reset_(bool reset_nextion) { | ||||||
| @@ -144,29 +145,33 @@ void Nextion::reset_(bool reset_nextion) { | |||||||
|  |  | ||||||
| void Nextion::dump_config() { | void Nextion::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "Nextion:"); |   ESP_LOGCONFIG(TAG, "Nextion:"); | ||||||
|   if (this->skip_connection_handshake_) { |  | ||||||
|     ESP_LOGCONFIG(TAG, "  Skip handshake: %s", YESNO(this->skip_connection_handshake_)); | #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE | ||||||
|   } else { |   ESP_LOGCONFIG(TAG, "  Skip handshake: YES"); | ||||||
|     ESP_LOGCONFIG(TAG, | #else  // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE | ||||||
|                   "  Device Model:   %s\n" |  | ||||||
|                   "  FW Version:     %s\n" |  | ||||||
|                   "  Serial Number:  %s\n" |  | ||||||
|                   "  Flash Size:     %s", |  | ||||||
|                   this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(), |  | ||||||
|                   this->flash_size_.c_str()); |  | ||||||
|   } |  | ||||||
|   ESP_LOGCONFIG(TAG, |   ESP_LOGCONFIG(TAG, | ||||||
|  | #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO | ||||||
|  |                 "  Device Model:   %s\n" | ||||||
|  |                 "  FW Version:     %s\n" | ||||||
|  |                 "  Serial Number:  %s\n" | ||||||
|  |                 "  Flash Size:     %s\n" | ||||||
|  | #endif  // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO | ||||||
|  | #ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START | ||||||
|  |                 "  Exit reparse:   YES\n" | ||||||
|  | #endif  // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START | ||||||
|                 "  Wake On Touch:  %s\n" |                 "  Wake On Touch:  %s\n" | ||||||
|                 "  Exit reparse:   %s", |                 "  Touch Timeout:  %" PRIu16, | ||||||
|                 YESNO(this->connection_state_.auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_)); | #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO | ||||||
|  |                 this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(), | ||||||
|  |                 this->flash_size_.c_str(), | ||||||
|  | #endif  // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO | ||||||
|  |                 YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_); | ||||||
|  | #endif  // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE | ||||||
|  |  | ||||||
| #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP | #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP | ||||||
|   ESP_LOGCONFIG(TAG, "  Max commands per loop: %u", this->max_commands_per_loop_); |   ESP_LOGCONFIG(TAG, "  Max commands per loop: %u", this->max_commands_per_loop_); | ||||||
| #endif  // USE_NEXTION_MAX_COMMANDS_PER_LOOP | #endif  // USE_NEXTION_MAX_COMMANDS_PER_LOOP | ||||||
|  |  | ||||||
|   if (this->touch_sleep_timeout_ != 0) { |  | ||||||
|     ESP_LOGCONFIG(TAG, "  Touch Timeout:  %" PRIu16, this->touch_sleep_timeout_); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (this->wake_up_page_ != 255) { |   if (this->wake_up_page_ != 255) { | ||||||
|     ESP_LOGCONFIG(TAG, "  Wake Up Page:   %u", this->wake_up_page_); |     ESP_LOGCONFIG(TAG, "  Wake Up Page:   %u", this->wake_up_page_); | ||||||
|   } |   } | ||||||
| @@ -314,6 +319,10 @@ void Nextion::loop() { | |||||||
|       this->set_wake_up_page(this->wake_up_page_); |       this->set_wake_up_page(this->wake_up_page_); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (this->touch_sleep_timeout_ != 0) { | ||||||
|  |       this->set_touch_sleep_timeout(this->touch_sleep_timeout_); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     this->connection_state_.ignore_is_setup_ = false; |     this->connection_state_.ignore_is_setup_ = false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -932,21 +932,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | |||||||
|    */ |    */ | ||||||
|   void set_backlight_brightness(float brightness); |   void set_backlight_brightness(float brightness); | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Sets whether the Nextion display should skip the connection handshake process. |  | ||||||
|    * @param skip_handshake True or false. When skip_connection_handshake is true, |  | ||||||
|    * the connection will be established without performing the handshake. |  | ||||||
|    * This can be useful when using Nextion Simulator. |  | ||||||
|    * |  | ||||||
|    * Example: |  | ||||||
|    * ```cpp |  | ||||||
|    * it.set_skip_connection_handshake(true); |  | ||||||
|    * ``` |  | ||||||
|    * |  | ||||||
|    * When set to true, the display will be marked as connected without performing a handshake. |  | ||||||
|    */ |  | ||||||
|   void set_skip_connection_handshake(bool skip_handshake) { this->skip_connection_handshake_ = skip_handshake; } |  | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Sets Nextion mode between sleep and awake |    * Sets Nextion mode between sleep and awake | ||||||
|    * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode. |    * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode. | ||||||
| @@ -1179,18 +1164,39 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | |||||||
|   void update_components_by_prefix(const std::string &prefix); |   void update_components_by_prefix(const std::string &prefix); | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Set the touch sleep timeout of the display. |    * Set the touch sleep timeout of the display using the `thsp` command. | ||||||
|    * @param timeout Timeout in seconds. |    * | ||||||
|  |    * Sets internal No-touch-then-sleep timer to specified value in seconds. | ||||||
|  |    * Nextion will auto-enter sleep mode if and when this timer expires. | ||||||
|  |    * | ||||||
|  |    * @param touch_sleep_timeout Timeout in seconds. | ||||||
|  |    *                           Range: 3 to 65535 seconds (minimum 3 seconds, maximum ~18 hours 12 minutes 15 seconds) | ||||||
|  |    *                           Use 0 to disable touch sleep timeout. | ||||||
|  |    * | ||||||
|  |    * @note Once `thsp` is set, it will persist until reboot or reset. The Nextion device | ||||||
|  |    *       needs to exit sleep mode to issue `thsp=0` to disable sleep on no touch. | ||||||
|  |    * | ||||||
|  |    * @note The display will only wake up by a restart or by setting up `thup` (auto wake on touch). | ||||||
|  |    *       See set_auto_wake_on_touch() to configure wake behavior. | ||||||
|    * |    * | ||||||
|    * Example: |    * Example: | ||||||
|    * ```cpp |    * ```cpp | ||||||
|  |    * // Set 30 second touch timeout | ||||||
|    * it.set_touch_sleep_timeout(30); |    * it.set_touch_sleep_timeout(30); | ||||||
|  |    * | ||||||
|  |    * // Set maximum timeout (~18 hours) | ||||||
|  |    * it.set_touch_sleep_timeout(65535); | ||||||
|  |    * | ||||||
|  |    * // Disable touch sleep timeout | ||||||
|  |    * it.set_touch_sleep_timeout(0); | ||||||
|    * ``` |    * ``` | ||||||
|    * |    * | ||||||
|    * After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up |    * Related Nextion instruction: `thsp=<value>` | ||||||
|    * `thup`. |    * | ||||||
|  |    * @see set_auto_wake_on_touch() Configure automatic wake on touch | ||||||
|  |    * @see sleep() Manually control sleep state | ||||||
|    */ |    */ | ||||||
|   void set_touch_sleep_timeout(uint16_t touch_sleep_timeout); |   void set_touch_sleep_timeout(uint16_t touch_sleep_timeout = 0); | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. |    * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. | ||||||
| @@ -1236,20 +1242,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | |||||||
|    */ |    */ | ||||||
|   void set_auto_wake_on_touch(bool auto_wake_on_touch); |   void set_auto_wake_on_touch(bool auto_wake_on_touch); | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Sets if Nextion should exit the active reparse mode before the "connect" command is sent |  | ||||||
|    * @param exit_reparse_on_start True or false. When exit_reparse_on_start is true, the exit reparse command |  | ||||||
|    * will be sent before requesting the connection from Nextion. |  | ||||||
|    * |  | ||||||
|    * Example: |  | ||||||
|    * ```cpp |  | ||||||
|    * it.set_exit_reparse_on_start(true); |  | ||||||
|    * ``` |  | ||||||
|    * |  | ||||||
|    * The display will be requested to leave active reparse mode before setup. |  | ||||||
|    */ |  | ||||||
|   void set_exit_reparse_on_start(bool exit_reparse_on_start) { this->exit_reparse_on_start_ = exit_reparse_on_start; } |  | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * @brief Retrieves the number of commands pending in the Nextion command queue. |    * @brief Retrieves the number of commands pending in the Nextion command queue. | ||||||
|    * |    * | ||||||
| @@ -1292,7 +1284,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | |||||||
|    * the Nextion display. A connection is considered established when: |    * the Nextion display. A connection is considered established when: | ||||||
|    * |    * | ||||||
|    * - The initial handshake with the display is completed successfully, or |    * - The initial handshake with the display is completed successfully, or | ||||||
|    * - The handshake is skipped via skip_connection_handshake_ flag |    * - The handshake is skipped via USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE flag | ||||||
|    * |    * | ||||||
|    * The connection status is particularly useful when: |    * The connection status is particularly useful when: | ||||||
|    * - Troubleshooting communication issues |    * - Troubleshooting communication issues | ||||||
| @@ -1358,8 +1350,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | |||||||
| #ifdef USE_NEXTION_CONF_START_UP_PAGE | #ifdef USE_NEXTION_CONF_START_UP_PAGE | ||||||
|   uint8_t start_up_page_ = 255; |   uint8_t start_up_page_ = 255; | ||||||
| #endif  // USE_NEXTION_CONF_START_UP_PAGE | #endif  // USE_NEXTION_CONF_START_UP_PAGE | ||||||
|   bool exit_reparse_on_start_ = false; |   bool auto_wake_on_touch_ = true; | ||||||
|   bool skip_connection_handshake_ = false; |  | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Manually send a raw command to the display and don't wait for an acknowledgement packet. |    * Manually send a raw command to the display and don't wait for an acknowledgement packet. | ||||||
| @@ -1466,10 +1457,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | |||||||
|   optional<nextion_writer_t> writer_; |   optional<nextion_writer_t> writer_; | ||||||
|   optional<float> brightness_; |   optional<float> brightness_; | ||||||
|  |  | ||||||
|  | #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO | ||||||
|   std::string device_model_; |   std::string device_model_; | ||||||
|   std::string firmware_version_; |   std::string firmware_version_; | ||||||
|   std::string serial_number_; |   std::string serial_number_; | ||||||
|   std::string flash_size_; |   std::string flash_size_; | ||||||
|  | #endif  // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO | ||||||
|  |  | ||||||
|   void remove_front_no_sensors_(); |   void remove_front_no_sensors_(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,14 +15,15 @@ void Nextion::set_wake_up_page(uint8_t wake_up_page) { | |||||||
|   this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); |   this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Nextion::set_touch_sleep_timeout(uint16_t touch_sleep_timeout) { | void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) { | ||||||
|   if (touch_sleep_timeout < 3) { |   // Validate range: Nextion thsp command requires min 3, max 65535 seconds (0 disables) | ||||||
|     ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)"); |   if (touch_sleep_timeout != 0 && touch_sleep_timeout < 3) { | ||||||
|     return; |     this->touch_sleep_timeout_ = 3;  // Auto-correct to minimum valid value | ||||||
|  |   } else { | ||||||
|  |     this->touch_sleep_timeout_ = touch_sleep_timeout; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->touch_sleep_timeout_ = touch_sleep_timeout; |   this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", this->touch_sleep_timeout_, true); | ||||||
|   this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", touch_sleep_timeout, true); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void Nextion::sleep(bool sleep) { | void Nextion::sleep(bool sleep) { | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								esphome/components/rp2040/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								esphome/components/rp2040/helpers.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/defines.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_RP2040 | ||||||
|  |  | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
|  | #if defined(USE_WIFI) | ||||||
|  | #include <WiFi.h> | ||||||
|  | #endif | ||||||
|  | #include <hardware/structs/rosc.h> | ||||||
|  | #include <hardware/sync.h> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  |  | ||||||
|  | uint32_t random_uint32() { | ||||||
|  |   uint32_t result = 0; | ||||||
|  |   for (uint8_t i = 0; i < 32; i++) { | ||||||
|  |     result <<= 1; | ||||||
|  |     result |= rosc_hw->randombit; | ||||||
|  |   } | ||||||
|  |   return result; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool random_bytes(uint8_t *data, size_t len) { | ||||||
|  |   while (len-- != 0) { | ||||||
|  |     uint8_t result = 0; | ||||||
|  |     for (uint8_t i = 0; i < 8; i++) { | ||||||
|  |       result <<= 1; | ||||||
|  |       result |= rosc_hw->randombit; | ||||||
|  |     } | ||||||
|  |     *data++ = result; | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RP2040 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. | ||||||
|  | Mutex::Mutex() {} | ||||||
|  | Mutex::~Mutex() {} | ||||||
|  | void Mutex::lock() {} | ||||||
|  | bool Mutex::try_lock() { return true; } | ||||||
|  | void Mutex::unlock() {} | ||||||
|  |  | ||||||
|  | IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } | ||||||
|  | IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } | ||||||
|  |  | ||||||
|  | void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) | ||||||
|  | #ifdef USE_WIFI | ||||||
|  |   WiFi.macAddress(mac); | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // USE_RP2040 | ||||||
							
								
								
									
										317
									
								
								esphome/components/sx126x/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								esphome/components/sx126x/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,317 @@ | |||||||
|  | from esphome import automation, pins | ||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.components import spi | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID | ||||||
|  | from esphome.core import TimePeriod | ||||||
|  |  | ||||||
|  | MULTI_CONF = True | ||||||
|  | CODEOWNERS = ["@swoboda1337"] | ||||||
|  | DEPENDENCIES = ["spi"] | ||||||
|  |  | ||||||
|  | CONF_SX126X_ID = "sx126x_id" | ||||||
|  |  | ||||||
|  | CONF_BANDWIDTH = "bandwidth" | ||||||
|  | CONF_BITRATE = "bitrate" | ||||||
|  | CONF_CODING_RATE = "coding_rate" | ||||||
|  | CONF_CRC_ENABLE = "crc_enable" | ||||||
|  | CONF_DEVIATION = "deviation" | ||||||
|  | CONF_DIO1_PIN = "dio1_pin" | ||||||
|  | CONF_HW_VERSION = "hw_version" | ||||||
|  | CONF_MODULATION = "modulation" | ||||||
|  | CONF_ON_PACKET = "on_packet" | ||||||
|  | CONF_PA_POWER = "pa_power" | ||||||
|  | CONF_PA_RAMP = "pa_ramp" | ||||||
|  | CONF_PAYLOAD_LENGTH = "payload_length" | ||||||
|  | CONF_PREAMBLE_DETECT = "preamble_detect" | ||||||
|  | CONF_PREAMBLE_SIZE = "preamble_size" | ||||||
|  | CONF_RST_PIN = "rst_pin" | ||||||
|  | CONF_RX_START = "rx_start" | ||||||
|  | CONF_RF_SWITCH = "rf_switch" | ||||||
|  | CONF_SHAPING = "shaping" | ||||||
|  | CONF_SPREADING_FACTOR = "spreading_factor" | ||||||
|  | CONF_SYNC_VALUE = "sync_value" | ||||||
|  | CONF_TCXO_VOLTAGE = "tcxo_voltage" | ||||||
|  | CONF_TCXO_DELAY = "tcxo_delay" | ||||||
|  |  | ||||||
|  | sx126x_ns = cg.esphome_ns.namespace("sx126x") | ||||||
|  | SX126x = sx126x_ns.class_("SX126x", cg.Component, spi.SPIDevice) | ||||||
|  | SX126xListener = sx126x_ns.class_("SX126xListener") | ||||||
|  | SX126xBw = sx126x_ns.enum("SX126xBw") | ||||||
|  | SX126xPacketType = sx126x_ns.enum("SX126xPacketType") | ||||||
|  | SX126xTcxoCtrl = sx126x_ns.enum("SX126xTcxoCtrl") | ||||||
|  | SX126xRampTime = sx126x_ns.enum("SX126xRampTime") | ||||||
|  | SX126xPulseShape = sx126x_ns.enum("SX126xPulseShape") | ||||||
|  | SX126xLoraCr = sx126x_ns.enum("SX126xLoraCr") | ||||||
|  |  | ||||||
|  | BW = { | ||||||
|  |     "4_8kHz": SX126xBw.SX126X_BW_4800, | ||||||
|  |     "5_8kHz": SX126xBw.SX126X_BW_5800, | ||||||
|  |     "7_3kHz": SX126xBw.SX126X_BW_7300, | ||||||
|  |     "9_7kHz": SX126xBw.SX126X_BW_9700, | ||||||
|  |     "11_7kHz": SX126xBw.SX126X_BW_11700, | ||||||
|  |     "14_6kHz": SX126xBw.SX126X_BW_14600, | ||||||
|  |     "19_5kHz": SX126xBw.SX126X_BW_19500, | ||||||
|  |     "23_4kHz": SX126xBw.SX126X_BW_23400, | ||||||
|  |     "29_3kHz": SX126xBw.SX126X_BW_29300, | ||||||
|  |     "39_0kHz": SX126xBw.SX126X_BW_39000, | ||||||
|  |     "46_9kHz": SX126xBw.SX126X_BW_46900, | ||||||
|  |     "58_6kHz": SX126xBw.SX126X_BW_58600, | ||||||
|  |     "78_2kHz": SX126xBw.SX126X_BW_78200, | ||||||
|  |     "93_8kHz": SX126xBw.SX126X_BW_93800, | ||||||
|  |     "117_3kHz": SX126xBw.SX126X_BW_117300, | ||||||
|  |     "156_2kHz": SX126xBw.SX126X_BW_156200, | ||||||
|  |     "187_2kHz": SX126xBw.SX126X_BW_187200, | ||||||
|  |     "234_3kHz": SX126xBw.SX126X_BW_234300, | ||||||
|  |     "312_0kHz": SX126xBw.SX126X_BW_312000, | ||||||
|  |     "373_6kHz": SX126xBw.SX126X_BW_373600, | ||||||
|  |     "467_0kHz": SX126xBw.SX126X_BW_467000, | ||||||
|  |     "7_8kHz": SX126xBw.SX126X_BW_7810, | ||||||
|  |     "10_4kHz": SX126xBw.SX126X_BW_10420, | ||||||
|  |     "15_6kHz": SX126xBw.SX126X_BW_15630, | ||||||
|  |     "20_8kHz": SX126xBw.SX126X_BW_20830, | ||||||
|  |     "31_3kHz": SX126xBw.SX126X_BW_31250, | ||||||
|  |     "41_7kHz": SX126xBw.SX126X_BW_41670, | ||||||
|  |     "62_5kHz": SX126xBw.SX126X_BW_62500, | ||||||
|  |     "125_0kHz": SX126xBw.SX126X_BW_125000, | ||||||
|  |     "250_0kHz": SX126xBw.SX126X_BW_250000, | ||||||
|  |     "500_0kHz": SX126xBw.SX126X_BW_500000, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | CODING_RATE = { | ||||||
|  |     "CR_4_5": SX126xLoraCr.LORA_CR_4_5, | ||||||
|  |     "CR_4_6": SX126xLoraCr.LORA_CR_4_6, | ||||||
|  |     "CR_4_7": SX126xLoraCr.LORA_CR_4_7, | ||||||
|  |     "CR_4_8": SX126xLoraCr.LORA_CR_4_8, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | MOD = { | ||||||
|  |     "LORA": SX126xPacketType.PACKET_TYPE_LORA, | ||||||
|  |     "FSK": SX126xPacketType.PACKET_TYPE_GFSK, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TCXO_VOLTAGE = { | ||||||
|  |     "1_6V": SX126xTcxoCtrl.TCXO_CTRL_1_6V, | ||||||
|  |     "1_7V": SX126xTcxoCtrl.TCXO_CTRL_1_7V, | ||||||
|  |     "1_8V": SX126xTcxoCtrl.TCXO_CTRL_1_8V, | ||||||
|  |     "2_2V": SX126xTcxoCtrl.TCXO_CTRL_2_2V, | ||||||
|  |     "2_4V": SX126xTcxoCtrl.TCXO_CTRL_2_4V, | ||||||
|  |     "2_7V": SX126xTcxoCtrl.TCXO_CTRL_2_7V, | ||||||
|  |     "3_0V": SX126xTcxoCtrl.TCXO_CTRL_3_0V, | ||||||
|  |     "3_3V": SX126xTcxoCtrl.TCXO_CTRL_3_3V, | ||||||
|  |     "NONE": SX126xTcxoCtrl.TCXO_CTRL_NONE, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | RAMP = { | ||||||
|  |     "10us": SX126xRampTime.PA_RAMP_10, | ||||||
|  |     "20us": SX126xRampTime.PA_RAMP_20, | ||||||
|  |     "40us": SX126xRampTime.PA_RAMP_40, | ||||||
|  |     "80us": SX126xRampTime.PA_RAMP_80, | ||||||
|  |     "200us": SX126xRampTime.PA_RAMP_200, | ||||||
|  |     "800us": SX126xRampTime.PA_RAMP_800, | ||||||
|  |     "1700us": SX126xRampTime.PA_RAMP_1700, | ||||||
|  |     "3400us": SX126xRampTime.PA_RAMP_3400, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SHAPING = { | ||||||
|  |     "GAUSSIAN_BT_0_3": SX126xPulseShape.GAUSSIAN_BT_0_3, | ||||||
|  |     "GAUSSIAN_BT_0_5": SX126xPulseShape.GAUSSIAN_BT_0_5, | ||||||
|  |     "GAUSSIAN_BT_0_7": SX126xPulseShape.GAUSSIAN_BT_0_7, | ||||||
|  |     "GAUSSIAN_BT_1_0": SX126xPulseShape.GAUSSIAN_BT_1_0, | ||||||
|  |     "NONE": SX126xPulseShape.NO_FILTER, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | RunImageCalAction = sx126x_ns.class_( | ||||||
|  |     "RunImageCalAction", automation.Action, cg.Parented.template(SX126x) | ||||||
|  | ) | ||||||
|  | SendPacketAction = sx126x_ns.class_( | ||||||
|  |     "SendPacketAction", automation.Action, cg.Parented.template(SX126x) | ||||||
|  | ) | ||||||
|  | SetModeTxAction = sx126x_ns.class_( | ||||||
|  |     "SetModeTxAction", automation.Action, cg.Parented.template(SX126x) | ||||||
|  | ) | ||||||
|  | SetModeRxAction = sx126x_ns.class_( | ||||||
|  |     "SetModeRxAction", automation.Action, cg.Parented.template(SX126x) | ||||||
|  | ) | ||||||
|  | SetModeSleepAction = sx126x_ns.class_( | ||||||
|  |     "SetModeSleepAction", automation.Action, cg.Parented.template(SX126x) | ||||||
|  | ) | ||||||
|  | SetModeStandbyAction = sx126x_ns.class_( | ||||||
|  |     "SetModeStandbyAction", automation.Action, cg.Parented.template(SX126x) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_raw_data(value): | ||||||
|  |     if isinstance(value, str): | ||||||
|  |         return value.encode("utf-8") | ||||||
|  |     if isinstance(value, list): | ||||||
|  |         return cv.Schema([cv.hex_uint8_t])(value) | ||||||
|  |     raise cv.Invalid( | ||||||
|  |         "data must either be a string wrapped in quotes or a list of bytes" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_config(config): | ||||||
|  |     lora_bws = [ | ||||||
|  |         "7_8kHz", | ||||||
|  |         "10_4kHz", | ||||||
|  |         "15_6kHz", | ||||||
|  |         "20_8kHz", | ||||||
|  |         "31_3kHz", | ||||||
|  |         "41_7kHz", | ||||||
|  |         "62_5kHz", | ||||||
|  |         "125_0kHz", | ||||||
|  |         "250_0kHz", | ||||||
|  |         "500_0kHz", | ||||||
|  |     ] | ||||||
|  |     if config[CONF_MODULATION] == "LORA": | ||||||
|  |         if config[CONF_BANDWIDTH] not in lora_bws: | ||||||
|  |             raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA") | ||||||
|  |         if config[CONF_PREAMBLE_SIZE] > 0 and config[CONF_PREAMBLE_SIZE] < 6: | ||||||
|  |             raise cv.Invalid("Minimum preamble size is 6 with LORA") | ||||||
|  |         if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0: | ||||||
|  |             raise cv.Invalid("Payload length must be set when spreading factor is 6") | ||||||
|  |     else: | ||||||
|  |         if config[CONF_BANDWIDTH] in lora_bws: | ||||||
|  |             raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with FSK") | ||||||
|  |         if config[CONF_PREAMBLE_DETECT] > len(config[CONF_SYNC_VALUE]): | ||||||
|  |             raise cv.Invalid("Preamble detection length must be <= sync value length") | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(SX126x), | ||||||
|  |             cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW), | ||||||
|  |             cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=600, max=300000), | ||||||
|  |             cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema, | ||||||
|  |             cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), | ||||||
|  |             cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, | ||||||
|  |             cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), | ||||||
|  |             cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema, | ||||||
|  |             cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), | ||||||
|  |             cv.Required(CONF_HW_VERSION): cv.one_of( | ||||||
|  |                 "sx1261", "sx1262", "sx1268", "llcc68", lower=True | ||||||
|  |             ), | ||||||
|  |             cv.Required(CONF_MODULATION): cv.enum(MOD), | ||||||
|  |             cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), | ||||||
|  |             cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=-3, max=22), | ||||||
|  |             cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP), | ||||||
|  |             cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256), | ||||||
|  |             cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4), | ||||||
|  |             cv.Required(CONF_PREAMBLE_SIZE): cv.int_range(min=1, max=65535), | ||||||
|  |             cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema, | ||||||
|  |             cv.Optional(CONF_RX_START, default=True): cv.boolean, | ||||||
|  |             cv.Required(CONF_RF_SWITCH): cv.boolean, | ||||||
|  |             cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING), | ||||||
|  |             cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12), | ||||||
|  |             cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t), | ||||||
|  |             cv.Optional(CONF_TCXO_VOLTAGE, default="NONE"): cv.enum(TCXO_VOLTAGE), | ||||||
|  |             cv.Optional(CONF_TCXO_DELAY, default="5ms"): cv.All( | ||||||
|  |                 cv.positive_time_period_microseconds, | ||||||
|  |                 cv.Range(max=TimePeriod(microseconds=262144000)), | ||||||
|  |             ), | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     .extend(cv.COMPONENT_SCHEMA) | ||||||
|  |     .extend(spi.spi_device_schema(True, 8e6, "mode0")) | ||||||
|  |     .add_extra(validate_config) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await spi.register_spi_device(var, config) | ||||||
|  |     if CONF_ON_PACKET in config: | ||||||
|  |         await automation.build_automation( | ||||||
|  |             var.get_packet_trigger(), | ||||||
|  |             [ | ||||||
|  |                 (cg.std_vector.template(cg.uint8), "x"), | ||||||
|  |                 (cg.float_, "rssi"), | ||||||
|  |                 (cg.float_, "snr"), | ||||||
|  |             ], | ||||||
|  |             config[CONF_ON_PACKET], | ||||||
|  |         ) | ||||||
|  |     if CONF_DIO1_PIN in config: | ||||||
|  |         dio1_pin = await cg.gpio_pin_expression(config[CONF_DIO1_PIN]) | ||||||
|  |         cg.add(var.set_dio1_pin(dio1_pin)) | ||||||
|  |     rst_pin = await cg.gpio_pin_expression(config[CONF_RST_PIN]) | ||||||
|  |     cg.add(var.set_rst_pin(rst_pin)) | ||||||
|  |     busy_pin = await cg.gpio_pin_expression(config[CONF_BUSY_PIN]) | ||||||
|  |     cg.add(var.set_busy_pin(busy_pin)) | ||||||
|  |     cg.add(var.set_bandwidth(config[CONF_BANDWIDTH])) | ||||||
|  |     cg.add(var.set_frequency(config[CONF_FREQUENCY])) | ||||||
|  |     cg.add(var.set_hw_version(config[CONF_HW_VERSION])) | ||||||
|  |     cg.add(var.set_deviation(config[CONF_DEVIATION])) | ||||||
|  |     cg.add(var.set_modulation(config[CONF_MODULATION])) | ||||||
|  |     cg.add(var.set_pa_ramp(config[CONF_PA_RAMP])) | ||||||
|  |     cg.add(var.set_pa_power(config[CONF_PA_POWER])) | ||||||
|  |     cg.add(var.set_shaping(config[CONF_SHAPING])) | ||||||
|  |     cg.add(var.set_bitrate(config[CONF_BITRATE])) | ||||||
|  |     cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE])) | ||||||
|  |     cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH])) | ||||||
|  |     cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE])) | ||||||
|  |     cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT])) | ||||||
|  |     cg.add(var.set_coding_rate(config[CONF_CODING_RATE])) | ||||||
|  |     cg.add(var.set_spreading_factor(config[CONF_SPREADING_FACTOR])) | ||||||
|  |     cg.add(var.set_sync_value(config[CONF_SYNC_VALUE])) | ||||||
|  |     cg.add(var.set_rx_start(config[CONF_RX_START])) | ||||||
|  |     cg.add(var.set_rf_switch(config[CONF_RF_SWITCH])) | ||||||
|  |     cg.add(var.set_tcxo_voltage(config[CONF_TCXO_VOLTAGE])) | ||||||
|  |     cg.add(var.set_tcxo_delay(config[CONF_TCXO_DELAY])) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.use_id(SX126x), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "sx126x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "sx126x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "sx126x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "sx126x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "sx126x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def no_args_action_to_code(config, action_id, template_arg, args): | ||||||
|  |     var = cg.new_Pvariable(action_id, template_arg) | ||||||
|  |     await cg.register_parented(var, config[CONF_ID]) | ||||||
|  |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.use_id(SX126x), | ||||||
|  |         cv.Required(CONF_DATA): cv.templatable(validate_raw_data), | ||||||
|  |     }, | ||||||
|  |     key=CONF_DATA, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "sx126x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def send_packet_action_to_code(config, action_id, template_arg, args): | ||||||
|  |     var = cg.new_Pvariable(action_id, template_arg) | ||||||
|  |     await cg.register_parented(var, config[CONF_ID]) | ||||||
|  |     data = config[CONF_DATA] | ||||||
|  |     if isinstance(data, bytes): | ||||||
|  |         data = list(data) | ||||||
|  |     if cg.is_template(data): | ||||||
|  |         templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) | ||||||
|  |         cg.add(var.set_data_template(templ)) | ||||||
|  |     else: | ||||||
|  |         cg.add(var.set_data_static(data)) | ||||||
|  |     return var | ||||||
							
								
								
									
										62
									
								
								esphome/components/sx126x/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								esphome/components/sx126x/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "esphome/components/sx126x/sx126x.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sx126x { | ||||||
|  |  | ||||||
|  | template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public Parented<SX126x> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->run_image_cal(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<SX126x> { | ||||||
|  |  public: | ||||||
|  |   void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) { | ||||||
|  |     this->data_func_ = func; | ||||||
|  |     this->static_ = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void set_data_static(const std::vector<uint8_t> &data) { | ||||||
|  |     this->data_static_ = data; | ||||||
|  |     this->static_ = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { | ||||||
|  |     if (this->static_) { | ||||||
|  |       this->parent_->transmit_packet(this->data_static_); | ||||||
|  |     } else { | ||||||
|  |       this->parent_->transmit_packet(this->data_func_(x...)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool static_{false}; | ||||||
|  |   std::function<std::vector<uint8_t>(Ts...)> data_func_{}; | ||||||
|  |   std::vector<uint8_t> data_static_{}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class SetModeTxAction : public Action<Ts...>, public Parented<SX126x> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->set_mode_tx(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class SetModeRxAction : public Action<Ts...>, public Parented<SX126x> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->set_mode_rx(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class SetModeSleepAction : public Action<Ts...>, public Parented<SX126x> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->set_mode_sleep(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, public Parented<SX126x> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->set_mode_standby(STDBY_XOSC); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sx126x | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										26
									
								
								esphome/components/sx126x/packet_transport/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/sx126x/packet_transport/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.components.packet_transport import ( | ||||||
|  |     PacketTransport, | ||||||
|  |     new_packet_transport, | ||||||
|  |     transport_schema, | ||||||
|  | ) | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.cpp_types import PollingComponent | ||||||
|  |  | ||||||
|  | from .. import CONF_SX126X_ID, SX126x, SX126xListener, sx126x_ns | ||||||
|  |  | ||||||
|  | SX126xTransport = sx126x_ns.class_( | ||||||
|  |     "SX126xTransport", PacketTransport, PollingComponent, SX126xListener | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = transport_schema(SX126xTransport).extend( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(CONF_SX126X_ID): cv.use_id(SX126x), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var, _ = await new_packet_transport(config) | ||||||
|  |     sx126x = await cg.get_variable(config[CONF_SX126X_ID]) | ||||||
|  |     cg.add(var.set_parent(sx126x)) | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/application.h" | ||||||
|  | #include "sx126x_transport.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sx126x { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "sx126x_transport"; | ||||||
|  |  | ||||||
|  | void SX126xTransport::setup() { | ||||||
|  |   PacketTransport::setup(); | ||||||
|  |   this->parent_->register_listener(this); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126xTransport::update() { | ||||||
|  |   PacketTransport::update(); | ||||||
|  |   this->updated_ = true; | ||||||
|  |   this->resend_data_ = true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126xTransport::send_packet(const std::vector<uint8_t> &buf) const { this->parent_->transmit_packet(buf); } | ||||||
|  |  | ||||||
|  | void SX126xTransport::on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) { this->process_(packet); } | ||||||
|  |  | ||||||
|  | }  // namespace sx126x | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/sx126x/sx126x.h" | ||||||
|  | #include "esphome/components/packet_transport/packet_transport.h" | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sx126x { | ||||||
|  |  | ||||||
|  | class SX126xTransport : public packet_transport::PacketTransport, public Parented<SX126x>, public SX126xListener { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   void update() override; | ||||||
|  |   void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void send_packet(const std::vector<uint8_t> &buf) const override; | ||||||
|  |   bool should_send() override { return true; } | ||||||
|  |   size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sx126x | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										523
									
								
								esphome/components/sx126x/sx126x.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										523
									
								
								esphome/components/sx126x/sx126x.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,523 @@ | |||||||
|  | #include "sx126x.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sx126x { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "sx126x"; | ||||||
|  | static const uint16_t RAMP[8] = {10, 20, 40, 80, 200, 800, 1700, 3400}; | ||||||
|  | static const uint32_t BW_HZ[31] = {4800,  5800,  7300,  9700,   11700,  14600,  19500,  23400,  29300,  39000,  46900, | ||||||
|  |                                    58600, 78200, 93800, 117300, 156200, 187200, 234300, 312000, 373600, 467000, 7810, | ||||||
|  |                                    10420, 15630, 20830, 31250,  41670,  62500,  125000, 250000, 500000}; | ||||||
|  | static const uint8_t BW_LORA[10] = {LORA_BW_7810,  LORA_BW_10420, LORA_BW_15630,  LORA_BW_20830,  LORA_BW_31250, | ||||||
|  |                                     LORA_BW_41670, LORA_BW_62500, LORA_BW_125000, LORA_BW_250000, LORA_BW_500000}; | ||||||
|  | static const uint8_t BW_FSK[21] = { | ||||||
|  |     FSK_BW_4800,   FSK_BW_5800,   FSK_BW_7300,   FSK_BW_9700,   FSK_BW_11700,  FSK_BW_14600,  FSK_BW_19500, | ||||||
|  |     FSK_BW_23400,  FSK_BW_29300,  FSK_BW_39000,  FSK_BW_46900,  FSK_BW_58600,  FSK_BW_78200,  FSK_BW_93800, | ||||||
|  |     FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000}; | ||||||
|  |  | ||||||
|  | static constexpr uint32_t RESET_DELAY_HIGH_US = 5000; | ||||||
|  | static constexpr uint32_t RESET_DELAY_LOW_US = 2000; | ||||||
|  | static constexpr uint32_t SWITCHING_DELAY_US = 1; | ||||||
|  | static constexpr uint32_t TRANSMIT_TIMEOUT_MS = 4000; | ||||||
|  | static constexpr uint32_t BUSY_TIMEOUT_MS = 20; | ||||||
|  |  | ||||||
|  | // OCP (Over Current Protection) values | ||||||
|  | static constexpr uint8_t OCP_80MA = 0x18;   // 80 mA max current | ||||||
|  | static constexpr uint8_t OCP_140MA = 0x38;  // 140 mA max current | ||||||
|  |  | ||||||
|  | // LoRa low data rate optimization threshold | ||||||
|  | static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f;  // 16.38 ms | ||||||
|  |  | ||||||
|  | uint8_t SX126x::read_fifo_(uint8_t offset, std::vector<uint8_t> &packet) { | ||||||
|  |   this->wait_busy_(); | ||||||
|  |   this->enable(); | ||||||
|  |   this->transfer_byte(RADIO_READ_BUFFER); | ||||||
|  |   this->transfer_byte(offset); | ||||||
|  |   uint8_t status = this->transfer_byte(0x00); | ||||||
|  |   for (uint8_t &byte : packet) { | ||||||
|  |     byte = this->transfer_byte(0x00); | ||||||
|  |   } | ||||||
|  |   this->disable(); | ||||||
|  |   return status; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::write_fifo_(uint8_t offset, const std::vector<uint8_t> &packet) { | ||||||
|  |   this->wait_busy_(); | ||||||
|  |   this->enable(); | ||||||
|  |   this->transfer_byte(RADIO_WRITE_BUFFER); | ||||||
|  |   this->transfer_byte(offset); | ||||||
|  |   for (const uint8_t &byte : packet) { | ||||||
|  |     this->transfer_byte(byte); | ||||||
|  |   } | ||||||
|  |   this->disable(); | ||||||
|  |   delayMicroseconds(SWITCHING_DELAY_US); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t SX126x::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) { | ||||||
|  |   this->wait_busy_(); | ||||||
|  |   this->enable(); | ||||||
|  |   this->transfer_byte(opcode); | ||||||
|  |   uint8_t status = this->transfer_byte(0x00); | ||||||
|  |   for (int32_t i = 0; i < size; i++) { | ||||||
|  |     data[i] = this->transfer_byte(0x00); | ||||||
|  |   } | ||||||
|  |   this->disable(); | ||||||
|  |   return status; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) { | ||||||
|  |   this->wait_busy_(); | ||||||
|  |   this->enable(); | ||||||
|  |   this->transfer_byte(opcode); | ||||||
|  |   for (int32_t i = 0; i < size; i++) { | ||||||
|  |     this->transfer_byte(data[i]); | ||||||
|  |   } | ||||||
|  |   this->disable(); | ||||||
|  |   delayMicroseconds(SWITCHING_DELAY_US); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::read_register_(uint16_t reg, uint8_t *data, uint8_t size) { | ||||||
|  |   this->wait_busy_(); | ||||||
|  |   this->enable(); | ||||||
|  |   this->write_byte(RADIO_READ_REGISTER); | ||||||
|  |   this->write_byte((reg >> 8) & 0xFF); | ||||||
|  |   this->write_byte((reg >> 0) & 0xFF); | ||||||
|  |   this->write_byte(0x00); | ||||||
|  |   for (int32_t i = 0; i < size; i++) { | ||||||
|  |     data[i] = this->transfer_byte(0x00); | ||||||
|  |   } | ||||||
|  |   this->disable(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) { | ||||||
|  |   this->wait_busy_(); | ||||||
|  |   this->enable(); | ||||||
|  |   this->write_byte(RADIO_WRITE_REGISTER); | ||||||
|  |   this->write_byte((reg >> 8) & 0xFF); | ||||||
|  |   this->write_byte((reg >> 0) & 0xFF); | ||||||
|  |   for (int32_t i = 0; i < size; i++) { | ||||||
|  |     this->transfer_byte(data[i]); | ||||||
|  |   } | ||||||
|  |   this->disable(); | ||||||
|  |   delayMicroseconds(SWITCHING_DELAY_US); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::setup() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "Running setup"); | ||||||
|  |  | ||||||
|  |   // setup pins | ||||||
|  |   this->busy_pin_->setup(); | ||||||
|  |   this->rst_pin_->setup(); | ||||||
|  |   this->dio1_pin_->setup(); | ||||||
|  |  | ||||||
|  |   // start spi | ||||||
|  |   this->spi_setup(); | ||||||
|  |  | ||||||
|  |   // configure rf | ||||||
|  |   this->configure(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::configure() { | ||||||
|  |   uint8_t buf[8]; | ||||||
|  |  | ||||||
|  |   // toggle chip reset | ||||||
|  |   this->rst_pin_->digital_write(true); | ||||||
|  |   delayMicroseconds(RESET_DELAY_HIGH_US); | ||||||
|  |   this->rst_pin_->digital_write(false); | ||||||
|  |   delayMicroseconds(RESET_DELAY_LOW_US); | ||||||
|  |   this->rst_pin_->digital_write(true); | ||||||
|  |   delayMicroseconds(RESET_DELAY_HIGH_US); | ||||||
|  |  | ||||||
|  |   // wakeup | ||||||
|  |   this->read_opcode_(RADIO_GET_STATUS, nullptr, 0); | ||||||
|  |  | ||||||
|  |   // config tcxo | ||||||
|  |   if (this->tcxo_voltage_ != TCXO_CTRL_NONE) { | ||||||
|  |     uint32_t delay = this->tcxo_delay_ >> 6; | ||||||
|  |     buf[0] = this->tcxo_voltage_; | ||||||
|  |     buf[1] = (delay >> 16) & 0xFF; | ||||||
|  |     buf[2] = (delay >> 8) & 0xFF; | ||||||
|  |     buf[3] = (delay >> 0) & 0xFF; | ||||||
|  |     this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4); | ||||||
|  |     buf[0] = 0x7F; | ||||||
|  |     this->write_opcode_(RADIO_CALIBRATE, buf, 1); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // clear errors | ||||||
|  |   buf[0] = 0x00; | ||||||
|  |   buf[1] = 0x00; | ||||||
|  |   this->write_opcode_(RADIO_CLR_ERROR, buf, 2); | ||||||
|  |  | ||||||
|  |   // rf switch | ||||||
|  |   if (this->rf_switch_) { | ||||||
|  |     buf[0] = 0x01; | ||||||
|  |     this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // check silicon version to make sure hw is ok | ||||||
|  |   this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, 16); | ||||||
|  |   if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) { | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // setup packet type | ||||||
|  |   buf[0] = this->modulation_; | ||||||
|  |   this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1); | ||||||
|  |  | ||||||
|  |   // calibrate image | ||||||
|  |   this->run_image_cal(); | ||||||
|  |  | ||||||
|  |   // set frequency | ||||||
|  |   uint64_t freq = ((uint64_t) this->frequency_ << 25) / XTAL_FREQ; | ||||||
|  |   buf[0] = (uint8_t) ((freq >> 24) & 0xFF); | ||||||
|  |   buf[1] = (uint8_t) ((freq >> 16) & 0xFF); | ||||||
|  |   buf[2] = (uint8_t) ((freq >> 8) & 0xFF); | ||||||
|  |   buf[3] = (uint8_t) (freq & 0xFF); | ||||||
|  |   this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4); | ||||||
|  |  | ||||||
|  |   // configure pa | ||||||
|  |   int8_t pa_power = this->pa_power_; | ||||||
|  |   if (this->hw_version_ == "sx1261") { | ||||||
|  |     // the following values were taken from section 13.1.14.1 table 13-21 | ||||||
|  |     // in rev 2.1 of the datasheet | ||||||
|  |     if (pa_power == 15) { | ||||||
|  |       uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01}; | ||||||
|  |       this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4); | ||||||
|  |     } else { | ||||||
|  |       uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01}; | ||||||
|  |       this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4); | ||||||
|  |     } | ||||||
|  |     pa_power = std::max(pa_power, (int8_t) -3); | ||||||
|  |     pa_power = std::min(pa_power, (int8_t) 14); | ||||||
|  |     buf[0] = OCP_80MA; | ||||||
|  |     this->write_register_(REG_OCP, buf, 1); | ||||||
|  |   } else { | ||||||
|  |     // the following values were taken from section 13.1.14.1 table 13-21 | ||||||
|  |     // in rev 2.1 of the datasheet | ||||||
|  |     uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01}; | ||||||
|  |     this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4); | ||||||
|  |     pa_power = std::max(pa_power, (int8_t) -3); | ||||||
|  |     pa_power = std::min(pa_power, (int8_t) 22); | ||||||
|  |     buf[0] = OCP_140MA; | ||||||
|  |     this->write_register_(REG_OCP, buf, 1); | ||||||
|  |   } | ||||||
|  |   buf[0] = pa_power; | ||||||
|  |   buf[1] = this->pa_ramp_; | ||||||
|  |   this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2); | ||||||
|  |  | ||||||
|  |   // configure modem | ||||||
|  |   if (this->modulation_ == PACKET_TYPE_LORA) { | ||||||
|  |     // set modulation params | ||||||
|  |     float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_]; | ||||||
|  |     buf[0] = this->spreading_factor_; | ||||||
|  |     buf[1] = BW_LORA[this->bandwidth_ - SX126X_BW_7810]; | ||||||
|  |     buf[2] = this->coding_rate_; | ||||||
|  |     buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00; | ||||||
|  |     this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4); | ||||||
|  |  | ||||||
|  |     // set packet params and sync word | ||||||
|  |     this->set_packet_params_(this->payload_length_); | ||||||
|  |     if (this->sync_value_.size() == 2) { | ||||||
|  |       this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     // set modulation params | ||||||
|  |     uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / this->bitrate_; | ||||||
|  |     uint32_t fdev = ((uint64_t) this->deviation_ << 25) / XTAL_FREQ; | ||||||
|  |     buf[0] = (bitrate >> 16) & 0xFF; | ||||||
|  |     buf[1] = (bitrate >> 8) & 0xFF; | ||||||
|  |     buf[2] = (bitrate >> 0) & 0xFF; | ||||||
|  |     buf[3] = this->shaping_; | ||||||
|  |     buf[4] = BW_FSK[this->bandwidth_ - SX126X_BW_4800]; | ||||||
|  |     buf[5] = (fdev >> 16) & 0xFF; | ||||||
|  |     buf[6] = (fdev >> 8) & 0xFF; | ||||||
|  |     buf[7] = (fdev >> 0) & 0xFF; | ||||||
|  |     this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8); | ||||||
|  |  | ||||||
|  |     // set packet params and sync word | ||||||
|  |     this->set_packet_params_(this->payload_length_); | ||||||
|  |     if (!this->sync_value_.empty()) { | ||||||
|  |       this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // switch to rx or sleep | ||||||
|  |   if (this->rx_start_) { | ||||||
|  |     this->set_mode_rx(); | ||||||
|  |   } else { | ||||||
|  |     this->set_mode_sleep(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | size_t SX126x::get_max_packet_size() { | ||||||
|  |   if (this->payload_length_ > 0) { | ||||||
|  |     return this->payload_length_; | ||||||
|  |   } | ||||||
|  |   return 255; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::set_packet_params_(uint8_t payload_length) { | ||||||
|  |   uint8_t buf[9]; | ||||||
|  |   if (this->modulation_ == PACKET_TYPE_LORA) { | ||||||
|  |     buf[0] = (this->preamble_size_ >> 8) & 0xFF; | ||||||
|  |     buf[1] = (this->preamble_size_ >> 0) & 0xFF; | ||||||
|  |     buf[2] = (this->payload_length_ > 0) ? 0x01 : 0x00; | ||||||
|  |     buf[3] = payload_length; | ||||||
|  |     buf[4] = (this->crc_enable_) ? 0x01 : 0x00; | ||||||
|  |     buf[5] = 0x00; | ||||||
|  |     this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 6); | ||||||
|  |   } else { | ||||||
|  |     uint16_t preamble_size = this->preamble_size_ * 8; | ||||||
|  |     buf[0] = (preamble_size >> 8) & 0xFF; | ||||||
|  |     buf[1] = (preamble_size >> 0) & 0xFF; | ||||||
|  |     buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00; | ||||||
|  |     buf[3] = this->sync_value_.size() * 8; | ||||||
|  |     buf[4] = 0x00; | ||||||
|  |     buf[5] = 0x00; | ||||||
|  |     buf[6] = payload_length; | ||||||
|  |     buf[7] = this->crc_enable_ ? 0x06 : 0x01; | ||||||
|  |     buf[8] = 0x00; | ||||||
|  |     this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SX126xError SX126x::transmit_packet(const std::vector<uint8_t> &packet) { | ||||||
|  |   if (this->payload_length_ > 0 && this->payload_length_ != packet.size()) { | ||||||
|  |     ESP_LOGE(TAG, "Packet size does not match config"); | ||||||
|  |     return SX126xError::INVALID_PARAMS; | ||||||
|  |   } | ||||||
|  |   if (packet.empty() || packet.size() > this->get_max_packet_size()) { | ||||||
|  |     ESP_LOGE(TAG, "Packet size out of range"); | ||||||
|  |     return SX126xError::INVALID_PARAMS; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   SX126xError ret = SX126xError::NONE; | ||||||
|  |   this->set_mode_standby(STDBY_XOSC); | ||||||
|  |   if (this->payload_length_ == 0) { | ||||||
|  |     this->set_packet_params_(packet.size()); | ||||||
|  |   } | ||||||
|  |   this->write_fifo_(0x00, packet); | ||||||
|  |   this->set_mode_tx(); | ||||||
|  |  | ||||||
|  |   // wait until transmit completes, typically the delay will be less than 100 ms | ||||||
|  |   uint32_t start = millis(); | ||||||
|  |   while (!this->dio1_pin_->digital_read()) { | ||||||
|  |     if (millis() - start > TRANSMIT_TIMEOUT_MS) { | ||||||
|  |       ESP_LOGE(TAG, "Transmit packet failure"); | ||||||
|  |       ret = SX126xError::TIMEOUT; | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint8_t buf[2]; | ||||||
|  |   buf[0] = 0xFF; | ||||||
|  |   buf[1] = 0xFF; | ||||||
|  |   this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2); | ||||||
|  |   if (this->rx_start_) { | ||||||
|  |     this->set_mode_rx(); | ||||||
|  |   } else { | ||||||
|  |     this->set_mode_sleep(); | ||||||
|  |   } | ||||||
|  |   return ret; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr) { | ||||||
|  |   for (auto &listener : this->listeners_) { | ||||||
|  |     listener->on_packet(packet, rssi, snr); | ||||||
|  |   } | ||||||
|  |   this->packet_trigger_->trigger(packet, rssi, snr); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::loop() { | ||||||
|  |   if (!this->dio1_pin_->digital_read()) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint16_t status; | ||||||
|  |   uint8_t buf[3]; | ||||||
|  |   uint8_t rssi; | ||||||
|  |   int8_t snr; | ||||||
|  |   this->read_opcode_(RADIO_GET_IRQSTATUS, buf, 2); | ||||||
|  |   this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2); | ||||||
|  |   status = (buf[0] << 8) | buf[1]; | ||||||
|  |   if ((status & IRQ_RX_DONE) == IRQ_RX_DONE) { | ||||||
|  |     if ((status & IRQ_CRC_ERROR) != IRQ_CRC_ERROR) { | ||||||
|  |       this->read_opcode_(RADIO_GET_PACKETSTATUS, buf, 3); | ||||||
|  |       if (this->modulation_ == PACKET_TYPE_LORA) { | ||||||
|  |         rssi = buf[0]; | ||||||
|  |         snr = buf[1]; | ||||||
|  |       } else { | ||||||
|  |         rssi = buf[2]; | ||||||
|  |         snr = 0; | ||||||
|  |       } | ||||||
|  |       this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, buf, 2); | ||||||
|  |       this->packet_.resize(buf[0]); | ||||||
|  |       this->read_fifo_(buf[1], this->packet_); | ||||||
|  |       this->call_listeners_(this->packet_, (float) rssi / -2.0f, (float) snr / 4.0f); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::run_image_cal() { | ||||||
|  |   // the following values were taken from section 9.2.1 table 9-2 | ||||||
|  |   // in rev 2.1 of the datasheet | ||||||
|  |   uint8_t buf[2] = {0, 0}; | ||||||
|  |   if (this->frequency_ > 900000000) { | ||||||
|  |     buf[0] = 0xE1; | ||||||
|  |     buf[1] = 0xE9; | ||||||
|  |   } else if (this->frequency_ > 850000000) { | ||||||
|  |     buf[0] = 0xD7; | ||||||
|  |     buf[1] = 0xD8; | ||||||
|  |   } else if (this->frequency_ > 770000000) { | ||||||
|  |     buf[0] = 0xC1; | ||||||
|  |     buf[1] = 0xC5; | ||||||
|  |   } else if (this->frequency_ > 460000000) { | ||||||
|  |     buf[0] = 0x75; | ||||||
|  |     buf[1] = 0x81; | ||||||
|  |   } else if (this->frequency_ > 425000000) { | ||||||
|  |     buf[0] = 0x6B; | ||||||
|  |     buf[1] = 0x6F; | ||||||
|  |   } | ||||||
|  |   if (buf[0] > 0 && buf[1] > 0) { | ||||||
|  |     this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::set_mode_rx() { | ||||||
|  |   uint8_t buf[8]; | ||||||
|  |  | ||||||
|  |   // configure irq params | ||||||
|  |   uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR; | ||||||
|  |   buf[0] = (irq >> 8) & 0xFF; | ||||||
|  |   buf[1] = (irq >> 0) & 0xFF; | ||||||
|  |   buf[2] = (irq >> 8) & 0xFF; | ||||||
|  |   buf[3] = (irq >> 0) & 0xFF; | ||||||
|  |   buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF; | ||||||
|  |   buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF; | ||||||
|  |   buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF; | ||||||
|  |   buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF; | ||||||
|  |   this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8); | ||||||
|  |  | ||||||
|  |   // set timeout to 0 | ||||||
|  |   buf[0] = 0x00; | ||||||
|  |   this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1); | ||||||
|  |  | ||||||
|  |   // switch to continuous mode rx | ||||||
|  |   buf[0] = 0xFF; | ||||||
|  |   buf[1] = 0xFF; | ||||||
|  |   buf[2] = 0xFF; | ||||||
|  |   this->write_opcode_(RADIO_SET_RX, buf, 3); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::set_mode_tx() { | ||||||
|  |   uint8_t buf[8]; | ||||||
|  |  | ||||||
|  |   // configure irq params | ||||||
|  |   uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT; | ||||||
|  |   buf[0] = (irq >> 8) & 0xFF; | ||||||
|  |   buf[1] = (irq >> 0) & 0xFF; | ||||||
|  |   buf[2] = (irq >> 8) & 0xFF; | ||||||
|  |   buf[3] = (irq >> 0) & 0xFF; | ||||||
|  |   buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF; | ||||||
|  |   buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF; | ||||||
|  |   buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF; | ||||||
|  |   buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF; | ||||||
|  |   this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8); | ||||||
|  |  | ||||||
|  |   // switch to single mode tx | ||||||
|  |   buf[0] = 0x00; | ||||||
|  |   buf[1] = 0x00; | ||||||
|  |   buf[2] = 0x00; | ||||||
|  |   this->write_opcode_(RADIO_SET_TX, buf, 3); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::set_mode_sleep() { | ||||||
|  |   uint8_t buf[1]; | ||||||
|  |   buf[0] = 0x05; | ||||||
|  |   this->write_opcode_(RADIO_SET_SLEEP, buf, 1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::set_mode_standby(SX126xStandbyMode mode) { | ||||||
|  |   uint8_t buf[1]; | ||||||
|  |   buf[0] = mode; | ||||||
|  |   this->write_opcode_(RADIO_SET_STANDBY, buf, 1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::wait_busy_() { | ||||||
|  |   // wait if the device is busy, the maximum delay is only be a few ms | ||||||
|  |   // with most commands taking only a few us | ||||||
|  |   uint32_t start = millis(); | ||||||
|  |   while (this->busy_pin_->digital_read()) { | ||||||
|  |     if (millis() - start > BUSY_TIMEOUT_MS) { | ||||||
|  |       ESP_LOGE(TAG, "Wait busy timeout"); | ||||||
|  |       this->mark_failed(); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SX126x::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "SX126x:"); | ||||||
|  |   LOG_PIN("  CS Pin: ", this->cs_); | ||||||
|  |   LOG_PIN("  BUSY Pin: ", this->busy_pin_); | ||||||
|  |   LOG_PIN("  RST Pin: ", this->rst_pin_); | ||||||
|  |   LOG_PIN("  DIO1 Pin: ", this->dio1_pin_); | ||||||
|  |   ESP_LOGCONFIG(TAG, | ||||||
|  |                 "  HW Version: %15s\n" | ||||||
|  |                 "  Frequency: %" PRIu32 " Hz\n" | ||||||
|  |                 "  Bandwidth: %" PRIu32 " Hz\n" | ||||||
|  |                 "  PA Power: %" PRId8 " dBm\n" | ||||||
|  |                 "  PA Ramp: %" PRIu16 " us\n" | ||||||
|  |                 "  Payload Length: %" PRIu32 "\n" | ||||||
|  |                 "  CRC Enable: %s\n" | ||||||
|  |                 "  Rx Start: %s", | ||||||
|  |                 this->version_, this->frequency_, BW_HZ[this->bandwidth_], this->pa_power_, RAMP[this->pa_ramp_], | ||||||
|  |                 this->payload_length_, TRUEFALSE(this->crc_enable_), TRUEFALSE(this->rx_start_)); | ||||||
|  |   if (this->modulation_ == PACKET_TYPE_GFSK) { | ||||||
|  |     const char *shaping = "NONE"; | ||||||
|  |     if (this->shaping_ == GAUSSIAN_BT_0_3) { | ||||||
|  |       shaping = "GAUSSIAN_BT_0_3"; | ||||||
|  |     } else if (this->shaping_ == GAUSSIAN_BT_0_5) { | ||||||
|  |       shaping = "GAUSSIAN_BT_0_5"; | ||||||
|  |     } else if (this->shaping_ == GAUSSIAN_BT_0_7) { | ||||||
|  |       shaping = "GAUSSIAN_BT_0_7"; | ||||||
|  |     } else if (this->shaping_ == GAUSSIAN_BT_1_0) { | ||||||
|  |       shaping = "GAUSSIAN_BT_1_0"; | ||||||
|  |     } | ||||||
|  |     ESP_LOGCONFIG(TAG, | ||||||
|  |                   "  Modulation: FSK\n" | ||||||
|  |                   "  Deviation: %" PRIu32 " Hz\n" | ||||||
|  |                   "  Shaping: %s\n" | ||||||
|  |                   "  Preamble Size: %" PRIu16 "\n" | ||||||
|  |                   "  Preamble Detect: %" PRIu16 "\n" | ||||||
|  |                   "  Bitrate: %" PRIu32 "b/s", | ||||||
|  |                   this->deviation_, shaping, this->preamble_size_, this->preamble_detect_, this->bitrate_); | ||||||
|  |   } else if (this->modulation_ == PACKET_TYPE_LORA) { | ||||||
|  |     const char *cr = "4/8"; | ||||||
|  |     if (this->coding_rate_ == LORA_CR_4_5) { | ||||||
|  |       cr = "4/5"; | ||||||
|  |     } else if (this->coding_rate_ == LORA_CR_4_6) { | ||||||
|  |       cr = "4/6"; | ||||||
|  |     } else if (this->coding_rate_ == LORA_CR_4_7) { | ||||||
|  |       cr = "4/7"; | ||||||
|  |     } | ||||||
|  |     ESP_LOGCONFIG(TAG, | ||||||
|  |                   "  Modulation: LORA\n" | ||||||
|  |                   "  Spreading Factor: %" PRIu8 "\n" | ||||||
|  |                   "  Coding Rate: %s\n" | ||||||
|  |                   "  Preamble Size: %" PRIu16, | ||||||
|  |                   this->spreading_factor_, cr, this->preamble_size_); | ||||||
|  |   } | ||||||
|  |   if (!this->sync_value_.empty()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); | ||||||
|  |   } | ||||||
|  |   if (this->is_failed()) { | ||||||
|  |     ESP_LOGE(TAG, "Configuring SX126x failed"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace sx126x | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										140
									
								
								esphome/components/sx126x/sx126x.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								esphome/components/sx126x/sx126x.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/spi/spi.h" | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "sx126x_reg.h" | ||||||
|  | #include <utility> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sx126x { | ||||||
|  |  | ||||||
|  | enum SX126xBw : uint8_t { | ||||||
|  |   // FSK | ||||||
|  |   SX126X_BW_4800, | ||||||
|  |   SX126X_BW_5800, | ||||||
|  |   SX126X_BW_7300, | ||||||
|  |   SX126X_BW_9700, | ||||||
|  |   SX126X_BW_11700, | ||||||
|  |   SX126X_BW_14600, | ||||||
|  |   SX126X_BW_19500, | ||||||
|  |   SX126X_BW_23400, | ||||||
|  |   SX126X_BW_29300, | ||||||
|  |   SX126X_BW_39000, | ||||||
|  |   SX126X_BW_46900, | ||||||
|  |   SX126X_BW_58600, | ||||||
|  |   SX126X_BW_78200, | ||||||
|  |   SX126X_BW_93800, | ||||||
|  |   SX126X_BW_117300, | ||||||
|  |   SX126X_BW_156200, | ||||||
|  |   SX126X_BW_187200, | ||||||
|  |   SX126X_BW_234300, | ||||||
|  |   SX126X_BW_312000, | ||||||
|  |   SX126X_BW_373600, | ||||||
|  |   SX126X_BW_467000, | ||||||
|  |   // LORA | ||||||
|  |   SX126X_BW_7810, | ||||||
|  |   SX126X_BW_10420, | ||||||
|  |   SX126X_BW_15630, | ||||||
|  |   SX126X_BW_20830, | ||||||
|  |   SX126X_BW_31250, | ||||||
|  |   SX126X_BW_41670, | ||||||
|  |   SX126X_BW_62500, | ||||||
|  |   SX126X_BW_125000, | ||||||
|  |   SX126X_BW_250000, | ||||||
|  |   SX126X_BW_500000, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS }; | ||||||
|  |  | ||||||
|  | class SX126xListener { | ||||||
|  |  public: | ||||||
|  |   virtual void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) = 0; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class SX126x : public Component, | ||||||
|  |                public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, | ||||||
|  |                                      spi::DATA_RATE_8MHZ> { | ||||||
|  |  public: | ||||||
|  |   size_t get_max_packet_size(); | ||||||
|  |   float get_setup_priority() const override { return setup_priority::PROCESSOR; } | ||||||
|  |   void setup() override; | ||||||
|  |   void loop() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; } | ||||||
|  |   void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; } | ||||||
|  |   void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; } | ||||||
|  |   void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; } | ||||||
|  |   void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; } | ||||||
|  |   void set_deviation(uint32_t deviation) { this->deviation_ = deviation; } | ||||||
|  |   void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; } | ||||||
|  |   void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } | ||||||
|  |   void set_hw_version(const std::string &hw_version) { this->hw_version_ = hw_version; } | ||||||
|  |   void set_mode_rx(); | ||||||
|  |   void set_mode_tx(); | ||||||
|  |   void set_mode_standby(SX126xStandbyMode mode); | ||||||
|  |   void set_mode_sleep(); | ||||||
|  |   void set_modulation(uint8_t modulation) { this->modulation_ = modulation; } | ||||||
|  |   void set_pa_power(int8_t power) { this->pa_power_ = power; } | ||||||
|  |   void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; } | ||||||
|  |   void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; } | ||||||
|  |   void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; } | ||||||
|  |   void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; } | ||||||
|  |   void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; } | ||||||
|  |   void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; } | ||||||
|  |   void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; } | ||||||
|  |   void set_shaping(uint8_t shaping) { this->shaping_ = shaping; } | ||||||
|  |   void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; } | ||||||
|  |   void set_sync_value(const std::vector<uint8_t> &sync_value) { this->sync_value_ = sync_value; } | ||||||
|  |   void set_tcxo_voltage(uint8_t tcxo_voltage) { this->tcxo_voltage_ = tcxo_voltage; } | ||||||
|  |   void set_tcxo_delay(uint32_t tcxo_delay) { this->tcxo_delay_ = tcxo_delay; } | ||||||
|  |   void run_image_cal(); | ||||||
|  |   void configure(); | ||||||
|  |   SX126xError transmit_packet(const std::vector<uint8_t> &packet); | ||||||
|  |   void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); } | ||||||
|  |   Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() const { return this->packet_trigger_; }; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void configure_fsk_ook_(); | ||||||
|  |   void configure_lora_(); | ||||||
|  |   void set_packet_params_(uint8_t payload_length); | ||||||
|  |   uint8_t read_fifo_(uint8_t offset, std::vector<uint8_t> &packet); | ||||||
|  |   void write_fifo_(uint8_t offset, const std::vector<uint8_t> &packet); | ||||||
|  |   void write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size); | ||||||
|  |   uint8_t read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size); | ||||||
|  |   void write_register_(uint16_t reg, uint8_t *data, uint8_t size); | ||||||
|  |   void read_register_(uint16_t reg, uint8_t *data, uint8_t size); | ||||||
|  |   void call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr); | ||||||
|  |   void wait_busy_(); | ||||||
|  |   Trigger<std::vector<uint8_t>, float, float> *packet_trigger_{new Trigger<std::vector<uint8_t>, float, float>()}; | ||||||
|  |   std::vector<SX126xListener *> listeners_; | ||||||
|  |   std::vector<uint8_t> packet_; | ||||||
|  |   std::vector<uint8_t> sync_value_; | ||||||
|  |   InternalGPIOPin *busy_pin_{nullptr}; | ||||||
|  |   InternalGPIOPin *dio1_pin_{nullptr}; | ||||||
|  |   InternalGPIOPin *rst_pin_{nullptr}; | ||||||
|  |   std::string hw_version_; | ||||||
|  |   char version_[16]; | ||||||
|  |   SX126xBw bandwidth_{SX126X_BW_125000}; | ||||||
|  |   uint32_t bitrate_{0}; | ||||||
|  |   uint32_t deviation_{0}; | ||||||
|  |   uint32_t frequency_{0}; | ||||||
|  |   uint32_t payload_length_{0}; | ||||||
|  |   uint32_t tcxo_delay_{0}; | ||||||
|  |   uint16_t preamble_detect_{0}; | ||||||
|  |   uint16_t preamble_size_{0}; | ||||||
|  |   uint8_t tcxo_voltage_{0}; | ||||||
|  |   uint8_t coding_rate_{0}; | ||||||
|  |   uint8_t modulation_{PACKET_TYPE_LORA}; | ||||||
|  |   uint8_t pa_ramp_{0}; | ||||||
|  |   uint8_t shaping_{0}; | ||||||
|  |   uint8_t spreading_factor_{0}; | ||||||
|  |   int8_t pa_power_{0}; | ||||||
|  |   bool crc_enable_{false}; | ||||||
|  |   bool rx_start_{false}; | ||||||
|  |   bool rf_switch_{false}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sx126x | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										163
									
								
								esphome/components/sx126x/sx126x_reg.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								esphome/components/sx126x/sx126x_reg.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sx126x { | ||||||
|  |  | ||||||
|  | static const uint32_t XTAL_FREQ = 32000000; | ||||||
|  |  | ||||||
|  | enum SX126xOpCode : uint8_t { | ||||||
|  |   RADIO_GET_STATUS = 0xC0, | ||||||
|  |   RADIO_WRITE_REGISTER = 0x0D, | ||||||
|  |   RADIO_READ_REGISTER = 0x1D, | ||||||
|  |   RADIO_WRITE_BUFFER = 0x0E, | ||||||
|  |   RADIO_READ_BUFFER = 0x1E, | ||||||
|  |   RADIO_SET_SLEEP = 0x84, | ||||||
|  |   RADIO_SET_STANDBY = 0x80, | ||||||
|  |   RADIO_SET_FS = 0xC1, | ||||||
|  |   RADIO_SET_TX = 0x83, | ||||||
|  |   RADIO_SET_RX = 0x82, | ||||||
|  |   RADIO_SET_RXDUTYCYCLE = 0x94, | ||||||
|  |   RADIO_SET_CAD = 0xC5, | ||||||
|  |   RADIO_SET_TXCONTINUOUSWAVE = 0xD1, | ||||||
|  |   RADIO_SET_TXCONTINUOUSPREAMBLE = 0xD2, | ||||||
|  |   RADIO_SET_PACKETTYPE = 0x8A, | ||||||
|  |   RADIO_GET_PACKETTYPE = 0x11, | ||||||
|  |   RADIO_SET_RFFREQUENCY = 0x86, | ||||||
|  |   RADIO_SET_TXPARAMS = 0x8E, | ||||||
|  |   RADIO_SET_PACONFIG = 0x95, | ||||||
|  |   RADIO_SET_CADPARAMS = 0x88, | ||||||
|  |   RADIO_SET_BUFFERBASEADDRESS = 0x8F, | ||||||
|  |   RADIO_SET_MODULATIONPARAMS = 0x8B, | ||||||
|  |   RADIO_SET_PACKETPARAMS = 0x8C, | ||||||
|  |   RADIO_GET_RXBUFFERSTATUS = 0x13, | ||||||
|  |   RADIO_GET_PACKETSTATUS = 0x14, | ||||||
|  |   RADIO_GET_RSSIINST = 0x15, | ||||||
|  |   RADIO_GET_STATS = 0x10, | ||||||
|  |   RADIO_RESET_STATS = 0x00, | ||||||
|  |   RADIO_SET_DIOIRQPARAMS = 0x08, | ||||||
|  |   RADIO_GET_IRQSTATUS = 0x12, | ||||||
|  |   RADIO_CLR_IRQSTATUS = 0x02, | ||||||
|  |   RADIO_CALIBRATE = 0x89, | ||||||
|  |   RADIO_CALIBRATEIMAGE = 0x98, | ||||||
|  |   RADIO_SET_REGULATORMODE = 0x96, | ||||||
|  |   RADIO_GET_ERROR = 0x17, | ||||||
|  |   RADIO_CLR_ERROR = 0x07, | ||||||
|  |   RADIO_SET_TCXOMODE = 0x97, | ||||||
|  |   RADIO_SET_TXFALLBACKMODE = 0x93, | ||||||
|  |   RADIO_SET_RFSWITCHMODE = 0x9D, | ||||||
|  |   RADIO_SET_STOPRXTIMERONPREAMBLE = 0x9F, | ||||||
|  |   RADIO_SET_LORASYMBTIMEOUT = 0xA0, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SX126xRegister : uint16_t { | ||||||
|  |   REG_VERSION_STRING = 0x0320, | ||||||
|  |   REG_GFSK_SYNCWORD = 0x06C0, | ||||||
|  |   REG_LORA_SYNCWORD = 0x0740, | ||||||
|  |   REG_OCP = 0x08E7, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SX126xStandbyMode : uint8_t { | ||||||
|  |   STDBY_RC = 0x00, | ||||||
|  |   STDBY_XOSC = 0x01, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SX126xPacketType : uint8_t { | ||||||
|  |   PACKET_TYPE_GFSK = 0x00, | ||||||
|  |   PACKET_TYPE_LORA = 0x01, | ||||||
|  |   PACKET_TYPE_LRHSS = 0x03, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SX126xFskBw : uint8_t { | ||||||
|  |   FSK_BW_4800 = 0x1F, | ||||||
|  |   FSK_BW_5800 = 0x17, | ||||||
|  |   FSK_BW_7300 = 0x0F, | ||||||
|  |   FSK_BW_9700 = 0x1E, | ||||||
|  |   FSK_BW_11700 = 0x16, | ||||||
|  |   FSK_BW_14600 = 0x0E, | ||||||
|  |   FSK_BW_19500 = 0x1D, | ||||||
|  |   FSK_BW_23400 = 0x15, | ||||||
|  |   FSK_BW_29300 = 0x0D, | ||||||
|  |   FSK_BW_39000 = 0x1C, | ||||||
|  |   FSK_BW_46900 = 0x14, | ||||||
|  |   FSK_BW_58600 = 0x0C, | ||||||
|  |   FSK_BW_78200 = 0x1B, | ||||||
|  |   FSK_BW_93800 = 0x13, | ||||||
|  |   FSK_BW_117300 = 0x0B, | ||||||
|  |   FSK_BW_156200 = 0x1A, | ||||||
|  |   FSK_BW_187200 = 0x12, | ||||||
|  |   FSK_BW_234300 = 0x0A, | ||||||
|  |   FSK_BW_312000 = 0x19, | ||||||
|  |   FSK_BW_373600 = 0x11, | ||||||
|  |   FSK_BW_467000 = 0x09, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SX126xLoraBw : uint8_t { | ||||||
|  |   LORA_BW_7810 = 0x00, | ||||||
|  |   LORA_BW_10420 = 0x08, | ||||||
|  |   LORA_BW_15630 = 0x01, | ||||||
|  |   LORA_BW_20830 = 0x09, | ||||||
|  |   LORA_BW_31250 = 0x02, | ||||||
|  |   LORA_BW_41670 = 0x0A, | ||||||
|  |   LORA_BW_62500 = 0x03, | ||||||
|  |   LORA_BW_125000 = 0x04, | ||||||
|  |   LORA_BW_250000 = 0x05, | ||||||
|  |   LORA_BW_500000 = 0x06, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SX126xLoraCr : uint8_t { | ||||||
|  |   LORA_CR_4_5 = 0x01, | ||||||
|  |   LORA_CR_4_6 = 0x02, | ||||||
|  |   LORA_CR_4_7 = 0x03, | ||||||
|  |   LORA_CR_4_8 = 0x04, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SX126xIrqMasks : uint16_t { | ||||||
|  |   IRQ_RADIO_NONE = 0x0000, | ||||||
|  |   IRQ_TX_DONE = 0x0001, | ||||||
|  |   IRQ_RX_DONE = 0x0002, | ||||||
|  |   IRQ_PREAMBLE_DETECTED = 0x0004, | ||||||
|  |   IRQ_SYNCWORD_VALID = 0x0008, | ||||||
|  |   IRQ_HEADER_VALID = 0x0010, | ||||||
|  |   IRQ_HEADER_ERROR = 0x0020, | ||||||
|  |   IRQ_CRC_ERROR = 0x0040, | ||||||
|  |   IRQ_CAD_DONE = 0x0080, | ||||||
|  |   IRQ_CAD_ACTIVITY_DETECTED = 0x0100, | ||||||
|  |   IRQ_RX_TX_TIMEOUT = 0x0200, | ||||||
|  |   IRQ_RADIO_ALL = 0xFFFF, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SX126xTcxoCtrl : uint8_t { | ||||||
|  |   TCXO_CTRL_1_6V = 0x00, | ||||||
|  |   TCXO_CTRL_1_7V = 0x01, | ||||||
|  |   TCXO_CTRL_1_8V = 0x02, | ||||||
|  |   TCXO_CTRL_2_2V = 0x03, | ||||||
|  |   TCXO_CTRL_2_4V = 0x04, | ||||||
|  |   TCXO_CTRL_2_7V = 0x05, | ||||||
|  |   TCXO_CTRL_3_0V = 0x06, | ||||||
|  |   TCXO_CTRL_3_3V = 0x07, | ||||||
|  |   TCXO_CTRL_NONE = 0xFF, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SX126xPulseShape : uint8_t { | ||||||
|  |   NO_FILTER = 0x00, | ||||||
|  |   GAUSSIAN_BT_0_3 = 0x08, | ||||||
|  |   GAUSSIAN_BT_0_5 = 0x09, | ||||||
|  |   GAUSSIAN_BT_0_7 = 0x0A, | ||||||
|  |   GAUSSIAN_BT_1_0 = 0x0B, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SX126xRampTime : uint8_t { | ||||||
|  |   PA_RAMP_10 = 0x00, | ||||||
|  |   PA_RAMP_20 = 0x01, | ||||||
|  |   PA_RAMP_40 = 0x02, | ||||||
|  |   PA_RAMP_80 = 0x03, | ||||||
|  |   PA_RAMP_200 = 0x04, | ||||||
|  |   PA_RAMP_800 = 0x05, | ||||||
|  |   PA_RAMP_1700 = 0x06, | ||||||
|  |   PA_RAMP_3400 = 0x07, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sx126x | ||||||
|  | }  // namespace esphome | ||||||
| @@ -12,47 +12,10 @@ | |||||||
| #include <cstdio> | #include <cstdio> | ||||||
| #include <cstring> | #include <cstring> | ||||||
|  |  | ||||||
| #ifdef USE_HOST |  | ||||||
| #ifndef _WIN32 |  | ||||||
| #include <net/if.h> |  | ||||||
| #include <netinet/in.h> |  | ||||||
| #include <sys/ioctl.h> |  | ||||||
| #endif |  | ||||||
| #include <unistd.h> |  | ||||||
| #endif |  | ||||||
| #if defined(USE_ESP8266) |  | ||||||
| #include <osapi.h> |  | ||||||
| #include <user_interface.h> |  | ||||||
| // for xt_rsil()/xt_wsr_ps() |  | ||||||
| #include <Arduino.h> |  | ||||||
| #elif defined(USE_ESP32_FRAMEWORK_ARDUINO) |  | ||||||
| #include <Esp.h> |  | ||||||
| #elif defined(USE_ESP_IDF) |  | ||||||
| #include <freertos/FreeRTOS.h> |  | ||||||
| #include <freertos/portmacro.h> |  | ||||||
| #include "esp_random.h" |  | ||||||
| #include "esp_system.h" |  | ||||||
| #elif defined(USE_RP2040) |  | ||||||
| #if defined(USE_WIFI) |  | ||||||
| #include <WiFi.h> |  | ||||||
| #endif |  | ||||||
| #include <hardware/structs/rosc.h> |  | ||||||
| #include <hardware/sync.h> |  | ||||||
| #elif defined(USE_HOST) |  | ||||||
| #include <limits> |  | ||||||
| #include <random> |  | ||||||
| #endif |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
| #include "esp_efuse.h" |  | ||||||
| #include "esp_efuse_table.h" |  | ||||||
| #include "esp_mac.h" |  | ||||||
| #include "rom/crc.h" | #include "rom/crc.h" | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_LIBRETINY |  | ||||||
| #include <WiFi.h>  // for macAddress() |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
|  |  | ||||||
| static const char *const TAG = "helpers"; | static const char *const TAG = "helpers"; | ||||||
| @@ -177,70 +140,7 @@ uint32_t fnv1_hash(const std::string &str) { | |||||||
|   return hash; |   return hash; | ||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 |  | ||||||
| uint32_t random_uint32() { return esp_random(); } |  | ||||||
| #elif defined(USE_ESP8266) |  | ||||||
| uint32_t random_uint32() { return os_random(); } |  | ||||||
| #elif defined(USE_RP2040) |  | ||||||
| uint32_t random_uint32() { |  | ||||||
|   uint32_t result = 0; |  | ||||||
|   for (uint8_t i = 0; i < 32; i++) { |  | ||||||
|     result <<= 1; |  | ||||||
|     result |= rosc_hw->randombit; |  | ||||||
|   } |  | ||||||
|   return result; |  | ||||||
| } |  | ||||||
| #elif defined(USE_LIBRETINY) |  | ||||||
| uint32_t random_uint32() { return rand(); } |  | ||||||
| #elif defined(USE_HOST) |  | ||||||
| uint32_t random_uint32() { |  | ||||||
|   std::random_device dev; |  | ||||||
|   std::mt19937 rng(dev()); |  | ||||||
|   std::uniform_int_distribution<uint32_t> dist(0, std::numeric_limits<uint32_t>::max()); |  | ||||||
|   return dist(rng); |  | ||||||
| } |  | ||||||
| #endif |  | ||||||
| float random_float() { return static_cast<float>(random_uint32()) / static_cast<float>(UINT32_MAX); } | float random_float() { return static_cast<float>(random_uint32()) / static_cast<float>(UINT32_MAX); } | ||||||
| #ifdef USE_ESP32 |  | ||||||
| bool random_bytes(uint8_t *data, size_t len) { |  | ||||||
|   esp_fill_random(data, len); |  | ||||||
|   return true; |  | ||||||
| } |  | ||||||
| #elif defined(USE_ESP8266) |  | ||||||
| bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; } |  | ||||||
| #elif defined(USE_RP2040) |  | ||||||
| bool random_bytes(uint8_t *data, size_t len) { |  | ||||||
|   while (len-- != 0) { |  | ||||||
|     uint8_t result = 0; |  | ||||||
|     for (uint8_t i = 0; i < 8; i++) { |  | ||||||
|       result <<= 1; |  | ||||||
|       result |= rosc_hw->randombit; |  | ||||||
|     } |  | ||||||
|     *data++ = result; |  | ||||||
|   } |  | ||||||
|   return true; |  | ||||||
| } |  | ||||||
| #elif defined(USE_LIBRETINY) |  | ||||||
| bool random_bytes(uint8_t *data, size_t len) { |  | ||||||
|   lt_rand_bytes(data, len); |  | ||||||
|   return true; |  | ||||||
| } |  | ||||||
| #elif defined(USE_HOST) |  | ||||||
| bool random_bytes(uint8_t *data, size_t len) { |  | ||||||
|   FILE *fp = fopen("/dev/urandom", "r"); |  | ||||||
|   if (fp == nullptr) { |  | ||||||
|     ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno); |  | ||||||
|     exit(1); |  | ||||||
|   } |  | ||||||
|   size_t read = fread(data, 1, len, fp); |  | ||||||
|   if (read != len) { |  | ||||||
|     ESP_LOGW(TAG, "Not enough data from /dev/urandom"); |  | ||||||
|     exit(1); |  | ||||||
|   } |  | ||||||
|   fclose(fp); |  | ||||||
|   return true; |  | ||||||
| } |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| // Strings | // Strings | ||||||
|  |  | ||||||
| @@ -644,42 +544,6 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green | |||||||
|   blue += delta; |   blue += delta; | ||||||
| } | } | ||||||
|  |  | ||||||
| // System APIs |  | ||||||
| #if defined(USE_ESP8266) || defined(USE_RP2040) |  | ||||||
| // ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. |  | ||||||
| Mutex::Mutex() {} |  | ||||||
| Mutex::~Mutex() {} |  | ||||||
| void Mutex::lock() {} |  | ||||||
| bool Mutex::try_lock() { return true; } |  | ||||||
| void Mutex::unlock() {} |  | ||||||
| #elif defined(USE_ESP32) || defined(USE_LIBRETINY) |  | ||||||
| Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } |  | ||||||
| Mutex::~Mutex() {} |  | ||||||
| void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } |  | ||||||
| bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } |  | ||||||
| void Mutex::unlock() { xSemaphoreGive(this->handle_); } |  | ||||||
| #elif defined(USE_HOST) |  | ||||||
| // Host platform uses std::mutex for proper thread synchronization |  | ||||||
| Mutex::Mutex() { handle_ = new std::mutex(); } |  | ||||||
| Mutex::~Mutex() { delete static_cast<std::mutex *>(handle_); } |  | ||||||
| void Mutex::lock() { static_cast<std::mutex *>(handle_)->lock(); } |  | ||||||
| bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock(); } |  | ||||||
| void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); } |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #if defined(USE_ESP8266) |  | ||||||
| IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } |  | ||||||
| IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } |  | ||||||
| #elif defined(USE_ESP32) || defined(USE_LIBRETINY) |  | ||||||
| // only affects the executing core |  | ||||||
| // so should not be used as a mutex lock, only to get accurate timing |  | ||||||
| IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } |  | ||||||
| IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } |  | ||||||
| #elif defined(USE_RP2040) |  | ||||||
| IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } |  | ||||||
| IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| uint8_t HighFrequencyLoopRequester::num_requests = 0;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | uint8_t HighFrequencyLoopRequester::num_requests = 0;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
| void HighFrequencyLoopRequester::start() { | void HighFrequencyLoopRequester::start() { | ||||||
|   if (this->started_) |   if (this->started_) | ||||||
| @@ -695,45 +559,6 @@ void HighFrequencyLoopRequester::stop() { | |||||||
| } | } | ||||||
| bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; } | bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; } | ||||||
|  |  | ||||||
| #if defined(USE_HOST) |  | ||||||
| void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) |  | ||||||
|   static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS; |  | ||||||
|   memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address)); |  | ||||||
| } |  | ||||||
| #elif defined(USE_ESP32) |  | ||||||
| void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) |  | ||||||
| #if defined(CONFIG_SOC_IEEE802154_SUPPORTED) |  | ||||||
|   // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default |  | ||||||
|   // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead. |  | ||||||
|   if (has_custom_mac_address()) { |  | ||||||
|     esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48); |  | ||||||
|   } else { |  | ||||||
|     esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); |  | ||||||
|   } |  | ||||||
| #else |  | ||||||
|   if (has_custom_mac_address()) { |  | ||||||
|     esp_efuse_mac_get_custom(mac); |  | ||||||
|   } else { |  | ||||||
|     esp_efuse_mac_get_default(mac); |  | ||||||
|   } |  | ||||||
| #endif |  | ||||||
| } |  | ||||||
| #elif defined(USE_ESP8266) |  | ||||||
| void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) |  | ||||||
|   wifi_get_macaddr(STATION_IF, mac); |  | ||||||
| } |  | ||||||
| #elif defined(USE_RP2040) |  | ||||||
| void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) |  | ||||||
| #ifdef USE_WIFI |  | ||||||
|   WiFi.macAddress(mac); |  | ||||||
| #endif |  | ||||||
| } |  | ||||||
| #elif defined(USE_LIBRETINY) |  | ||||||
| void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter) |  | ||||||
|   WiFi.macAddress(mac); |  | ||||||
| } |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| std::string get_mac_address() { | std::string get_mac_address() { | ||||||
|   uint8_t mac[6]; |   uint8_t mac[6]; | ||||||
|   get_mac_address_raw(mac); |   get_mac_address_raw(mac); | ||||||
| @@ -746,24 +571,10 @@ std::string get_mac_address_pretty() { | |||||||
|   return format_mac_address_pretty(mac); |   return format_mac_address_pretty(mac); | ||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifndef USE_ESP32 | ||||||
| void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } | bool has_custom_mac_address() { return false; } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| bool has_custom_mac_address() { |  | ||||||
| #if defined(USE_ESP32) && !defined(USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC) |  | ||||||
|   uint8_t mac[6]; |  | ||||||
|   // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails |  | ||||||
| #ifndef USE_ESP32_VARIANT_ESP32 |  | ||||||
|   return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); |  | ||||||
| #else |  | ||||||
|   return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); |  | ||||||
| #endif |  | ||||||
| #else |  | ||||||
|   return false; |  | ||||||
| #endif |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool mac_address_is_valid(const uint8_t *mac) { | bool mac_address_is_valid(const uint8_t *mac) { | ||||||
|   bool is_all_zeros = true; |   bool is_all_zeros = true; | ||||||
|   bool is_all_ones = true; |   bool is_all_ones = true; | ||||||
|   | |||||||
| @@ -62,16 +62,16 @@ static void validate_static_string(const char *name) { | |||||||
| void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, | void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, | ||||||
|                                       const void *name_ptr, uint32_t delay, std::function<void()> func) { |                                       const void *name_ptr, uint32_t delay, std::function<void()> func) { | ||||||
|   // Get the name as const char* |   // Get the name as const char* | ||||||
|   const char *name_cstr = |   const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); | ||||||
|       is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str(); |  | ||||||
|  |  | ||||||
|   // Cancel existing timer if name is not empty |   if (delay == SCHEDULER_DONT_RUN) { | ||||||
|   if (name_cstr != nullptr && name_cstr[0] != '\0') { |     // Still need to cancel existing timer if name is not empty | ||||||
|     this->cancel_item_(component, name_cstr, type); |     if (this->is_name_valid_(name_cstr)) { | ||||||
|   } |       LockGuard guard{this->lock_}; | ||||||
|  |       this->cancel_item_locked_(component, name_cstr, type); | ||||||
|   if (delay == SCHEDULER_DONT_RUN) |     } | ||||||
|     return; |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Create and populate the scheduler item |   // Create and populate the scheduler item | ||||||
|   auto item = make_unique<SchedulerItem>(); |   auto item = make_unique<SchedulerItem>(); | ||||||
| @@ -87,6 +87,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type | |||||||
|   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 | ||||||
|     LockGuard guard{this->lock_}; |     LockGuard guard{this->lock_}; | ||||||
|  |     this->cancel_item_locked_(component, name_cstr, type); | ||||||
|     this->defer_queue_.push_back(std::move(item)); |     this->defer_queue_.push_back(std::move(item)); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| @@ -122,7 +123,15 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type | |||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   this->push_(std::move(item)); |   LockGuard guard{this->lock_}; | ||||||
|  |   // If name is provided, do atomic cancel-and-add | ||||||
|  |   if (this->is_name_valid_(name_cstr)) { | ||||||
|  |     // Cancel existing items | ||||||
|  |     this->cancel_item_locked_(component, name_cstr, type); | ||||||
|  |   } | ||||||
|  |   // Add new item directly to to_add_ | ||||||
|  |   // 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) { | ||||||
| @@ -134,10 +143,10 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u | |||||||
|   this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func)); |   this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func)); | ||||||
| } | } | ||||||
| bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { | bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { | ||||||
|   return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); |   return this->cancel_item_(component, false, &name, SchedulerItem::TIMEOUT); | ||||||
| } | } | ||||||
| bool HOT Scheduler::cancel_timeout(Component *component, const char *name) { | bool HOT Scheduler::cancel_timeout(Component *component, const char *name) { | ||||||
|   return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); |   return this->cancel_item_(component, true, name, SchedulerItem::TIMEOUT); | ||||||
| } | } | ||||||
| void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, | void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, | ||||||
|                                  std::function<void()> func) { |                                  std::function<void()> func) { | ||||||
| @@ -149,10 +158,10 @@ void HOT Scheduler::set_interval(Component *component, const char *name, uint32_ | |||||||
|   this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func)); |   this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func)); | ||||||
| } | } | ||||||
| bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { | bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { | ||||||
|   return this->cancel_item_(component, name, SchedulerItem::INTERVAL); |   return this->cancel_item_(component, false, &name, SchedulerItem::INTERVAL); | ||||||
| } | } | ||||||
| bool HOT Scheduler::cancel_interval(Component *component, const char *name) { | bool HOT Scheduler::cancel_interval(Component *component, const char *name) { | ||||||
|   return this->cancel_item_(component, name, SchedulerItem::INTERVAL); |   return this->cancel_item_(component, true, name, SchedulerItem::INTERVAL); | ||||||
| } | } | ||||||
|  |  | ||||||
| struct RetryArgs { | struct RetryArgs { | ||||||
| @@ -211,6 +220,9 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) | |||||||
| } | } | ||||||
|  |  | ||||||
| optional<uint32_t> HOT Scheduler::next_schedule_in() { | optional<uint32_t> HOT Scheduler::next_schedule_in() { | ||||||
|  |   // IMPORTANT: This method should only be called from the main thread (loop task). | ||||||
|  |   // It calls empty_() and accesses items_[0] without holding a lock, which is only | ||||||
|  |   // safe when called from the main thread. Other threads must not call this method. | ||||||
|   if (this->empty_()) |   if (this->empty_()) | ||||||
|     return {}; |     return {}; | ||||||
|   auto &item = this->items_[0]; |   auto &item = this->items_[0]; | ||||||
| @@ -230,6 +242,10 @@ void HOT Scheduler::call() { | |||||||
|   // - No deferred items exist in to_add_, so processing order doesn't affect correctness |   // - No deferred items exist in to_add_, so processing order doesn't affect correctness | ||||||
|   // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach |   // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach | ||||||
|   // (ESP8266: single-core, RP2040: empty mutex implementation). |   // (ESP8266: single-core, RP2040: empty mutex implementation). | ||||||
|  |   // | ||||||
|  |   // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still | ||||||
|  |   // processed here. They are removed from the queue normally via pop_front() but skipped | ||||||
|  |   // during execution by should_skip_item_(). This is intentional - no memory leak occurs. | ||||||
|   while (!this->defer_queue_.empty()) { |   while (!this->defer_queue_.empty()) { | ||||||
|     // The outer check is done without a lock for performance. If the queue |     // The outer check is done without a lock for performance. If the queue | ||||||
|     // appears non-empty, we lock and process an item. We don't need to check |     // appears non-empty, we lock and process an item. We don't need to check | ||||||
| @@ -261,10 +277,12 @@ void HOT Scheduler::call() { | |||||||
|     ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, |     ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, | ||||||
|              this->last_millis_); |              this->last_millis_); | ||||||
|     while (!this->empty_()) { |     while (!this->empty_()) { | ||||||
|       this->lock_.lock(); |       std::unique_ptr<SchedulerItem> item; | ||||||
|       auto item = std::move(this->items_[0]); |       { | ||||||
|       this->pop_raw_(); |         LockGuard guard{this->lock_}; | ||||||
|       this->lock_.unlock(); |         item = std::move(this->items_[0]); | ||||||
|  |         this->pop_raw_(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       const char *name = item->get_name(); |       const char *name = item->get_name(); | ||||||
|       ESP_LOGD(TAG, "  %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, |       ESP_LOGD(TAG, "  %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, | ||||||
| @@ -278,33 +296,35 @@ void HOT Scheduler::call() { | |||||||
|     { |     { | ||||||
|       LockGuard guard{this->lock_}; |       LockGuard guard{this->lock_}; | ||||||
|       this->items_ = std::move(old_items); |       this->items_ = std::move(old_items); | ||||||
|  |       // Rebuild heap after moving items back | ||||||
|  |       std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| #endif  // ESPHOME_DEBUG_SCHEDULER | #endif  // ESPHOME_DEBUG_SCHEDULER | ||||||
|  |  | ||||||
|   auto to_remove_was = to_remove_; |  | ||||||
|   auto items_was = this->items_.size(); |  | ||||||
|   // If we have too many items to remove |   // If we have too many items to remove | ||||||
|   if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { |   if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { | ||||||
|  |     // We hold the lock for the entire cleanup operation because: | ||||||
|  |     // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout | ||||||
|  |     // 2. Other threads must see either the old state or the new state, not intermediate states | ||||||
|  |     // 3. The operation is already expensive (O(n)), so lock overhead is negligible | ||||||
|  |     // 4. No operations inside can block or take other locks, so no deadlock risk | ||||||
|  |     LockGuard guard{this->lock_}; | ||||||
|  |  | ||||||
|     std::vector<std::unique_ptr<SchedulerItem>> valid_items; |     std::vector<std::unique_ptr<SchedulerItem>> valid_items; | ||||||
|     while (!this->empty_()) { |  | ||||||
|       LockGuard guard{this->lock_}; |     // Move all non-removed items to valid_items | ||||||
|       auto item = std::move(this->items_[0]); |     for (auto &item : this->items_) { | ||||||
|       this->pop_raw_(); |       if (!item->remove) { | ||||||
|       valid_items.push_back(std::move(item)); |         valid_items.push_back(std::move(item)); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     { |     // Replace items_ with the filtered list | ||||||
|       LockGuard guard{this->lock_}; |     this->items_ = std::move(valid_items); | ||||||
|       this->items_ = std::move(valid_items); |     // Rebuild the heap structure since items are no longer in heap order | ||||||
|     } |     std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); | ||||||
|  |     this->to_remove_ = 0; | ||||||
|     // The following should not happen unless I'm missing something |  | ||||||
|     if (to_remove_ != 0) { |  | ||||||
|       ESP_LOGW(TAG, "to_remove_ was %" PRIu32 " now: %" PRIu32 " items where %zu now %zu. Please report this", |  | ||||||
|                to_remove_was, to_remove_, items_was, items_.size()); |  | ||||||
|       to_remove_ = 0; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   while (!this->empty_()) { |   while (!this->empty_()) { | ||||||
| @@ -336,26 +356,25 @@ void HOT Scheduler::call() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     { |     { | ||||||
|       this->lock_.lock(); |       LockGuard guard{this->lock_}; | ||||||
|  |  | ||||||
|       // new scope, item from before might have been moved in the vector |       // new scope, item from before might have been moved in the vector | ||||||
|       auto item = std::move(this->items_[0]); |       auto item = std::move(this->items_[0]); | ||||||
|  |  | ||||||
|       // Only pop after function call, this ensures we were reachable |       // Only pop after function call, this ensures we were reachable | ||||||
|       // during the function call and know if we were cancelled. |       // during the function call and know if we were cancelled. | ||||||
|       this->pop_raw_(); |       this->pop_raw_(); | ||||||
|  |  | ||||||
|       this->lock_.unlock(); |  | ||||||
|  |  | ||||||
|       if (item->remove) { |       if (item->remove) { | ||||||
|         // We were removed/cancelled in the function call, stop |         // We were removed/cancelled in the function call, stop | ||||||
|         to_remove_--; |         this->to_remove_--; | ||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (item->type == SchedulerItem::INTERVAL) { |       if (item->type == SchedulerItem::INTERVAL) { | ||||||
|         item->next_execution_ = now + item->interval; |         item->next_execution_ = now + item->interval; | ||||||
|         this->push_(std::move(item)); |         // Add new item directly to to_add_ | ||||||
|  |         // since we have the lock held | ||||||
|  |         this->to_add_.push_back(std::move(item)); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -375,36 +394,37 @@ void HOT Scheduler::process_to_add() { | |||||||
|   this->to_add_.clear(); |   this->to_add_.clear(); | ||||||
| } | } | ||||||
| void HOT Scheduler::cleanup_() { | void HOT Scheduler::cleanup_() { | ||||||
|  |   // Fast path: if nothing to remove, just return | ||||||
|  |   // Reading to_remove_ without lock is safe because: | ||||||
|  |   // 1. We only call this from the main thread during call() | ||||||
|  |   // 2. If it's 0, there's definitely nothing to cleanup | ||||||
|  |   // 3. If it becomes non-zero after we check, cleanup will happen on the next loop iteration | ||||||
|  |   // 4. Not all platforms support atomics, so we accept this race in favor of performance | ||||||
|  |   // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless | ||||||
|  |   if (this->to_remove_ == 0) | ||||||
|  |     return; | ||||||
|  |  | ||||||
|  |   // We must hold the lock for the entire cleanup operation because: | ||||||
|  |   // 1. We're modifying items_ (via pop_raw_) which requires exclusive access | ||||||
|  |   // 2. We're decrementing to_remove_ which is also modified by other threads | ||||||
|  |   //    (though all modifications are already under lock) | ||||||
|  |   // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_() | ||||||
|  |   // 4. We need a consistent view of items_ and to_remove_ throughout the operation | ||||||
|  |   // Without the lock, we could access items_ while another thread is reading it, | ||||||
|  |   // leading to race conditions | ||||||
|  |   LockGuard guard{this->lock_}; | ||||||
|   while (!this->items_.empty()) { |   while (!this->items_.empty()) { | ||||||
|     auto &item = this->items_[0]; |     auto &item = this->items_[0]; | ||||||
|     if (!item->remove) |     if (!item->remove) | ||||||
|       return; |       return; | ||||||
|  |     this->to_remove_--; | ||||||
|     to_remove_--; |     this->pop_raw_(); | ||||||
|  |  | ||||||
|     { |  | ||||||
|       LockGuard guard{this->lock_}; |  | ||||||
|       this->pop_raw_(); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| void HOT Scheduler::pop_raw_() { | void HOT Scheduler::pop_raw_() { | ||||||
|   std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); |   std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); | ||||||
|   this->items_.pop_back(); |   this->items_.pop_back(); | ||||||
| } | } | ||||||
| void HOT Scheduler::push_(std::unique_ptr<Scheduler::SchedulerItem> item) { |  | ||||||
|   LockGuard guard{this->lock_}; |  | ||||||
|   this->to_add_.push_back(std::move(item)); |  | ||||||
| } |  | ||||||
| // Helper function to check if item matches criteria for cancellation |  | ||||||
| bool HOT Scheduler::matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, |  | ||||||
|                                   const char *name_cstr, SchedulerItem::Type type) { |  | ||||||
|   if (item->component != component || item->type != type || item->remove) { |  | ||||||
|     return false; |  | ||||||
|   } |  | ||||||
|   const char *item_name = item->get_name(); |  | ||||||
|   return item_name != nullptr && strcmp(name_cstr, item_name) == 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Helper to execute a scheduler item | // Helper to execute a scheduler item | ||||||
| void HOT Scheduler::execute_item_(SchedulerItem *item) { | void HOT Scheduler::execute_item_(SchedulerItem *item) { | ||||||
| @@ -417,55 +437,56 @@ void HOT Scheduler::execute_item_(SchedulerItem *item) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Common implementation for cancel operations | // Common implementation for cancel operations | ||||||
| bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, | bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, const void *name_ptr, | ||||||
|                                         SchedulerItem::Type type) { |                                  SchedulerItem::Type type) { | ||||||
|   // Get the name as const char* |   // Get the name as const char* | ||||||
|   const char *name_cstr = |   const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); | ||||||
|       is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str(); |  | ||||||
|  |  | ||||||
|   // Handle null or empty names |   // Handle null or empty names | ||||||
|   if (name_cstr == nullptr) |   if (!this->is_name_valid_(name_cstr)) | ||||||
|     return false; |     return false; | ||||||
|  |  | ||||||
|   // obtain lock because this function iterates and can be called from non-loop task context |   // obtain lock because this function iterates and can be called from non-loop task context | ||||||
|   LockGuard guard{this->lock_}; |   LockGuard guard{this->lock_}; | ||||||
|   bool ret = false; |   return this->cancel_item_locked_(component, name_cstr, type); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Helper to cancel items by name - must be called with lock held | ||||||
|  | bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) { | ||||||
|  |   size_t total_cancelled = 0; | ||||||
|  |  | ||||||
|   // Check all containers for matching items |   // Check all containers for matching items | ||||||
| #if !defined(USE_ESP8266) && !defined(USE_RP2040) | #if !defined(USE_ESP8266) && !defined(USE_RP2040) | ||||||
|   // Only check defer_queue_ on platforms that have it |   // Only check defer queue for timeouts (intervals never go there) | ||||||
|   for (auto &item : this->defer_queue_) { |   if (type == SchedulerItem::TIMEOUT) { | ||||||
|     if (this->matches_item_(item, component, name_cstr, type)) { |     for (auto &item : this->defer_queue_) { | ||||||
|       item->remove = true; |       if (this->matches_item_(item, component, name_cstr, type)) { | ||||||
|       ret = true; |         item->remove = true; | ||||||
|  |         total_cancelled++; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  |   // Cancel items in the main heap | ||||||
|   for (auto &item : this->items_) { |   for (auto &item : this->items_) { | ||||||
|     if (this->matches_item_(item, component, name_cstr, type)) { |     if (this->matches_item_(item, component, name_cstr, type)) { | ||||||
|       item->remove = true; |       item->remove = true; | ||||||
|       ret = true; |       total_cancelled++; | ||||||
|       this->to_remove_++;  // Only track removals for heap items |       this->to_remove_++;  // Track removals for heap items | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Cancel items in to_add_ | ||||||
|   for (auto &item : this->to_add_) { |   for (auto &item : this->to_add_) { | ||||||
|     if (this->matches_item_(item, component, name_cstr, type)) { |     if (this->matches_item_(item, component, name_cstr, type)) { | ||||||
|       item->remove = true; |       item->remove = true; | ||||||
|       ret = true; |       total_cancelled++; | ||||||
|  |       // Don't track removals for to_add_ items | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ret; |   return total_cancelled > 0; | ||||||
| } |  | ||||||
|  |  | ||||||
| bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { |  | ||||||
|   return this->cancel_item_common_(component, false, &name, type); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool HOT Scheduler::cancel_item_(Component *component, const char *name, SchedulerItem::Type type) { |  | ||||||
|   return this->cancel_item_common_(component, true, name, type); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| uint64_t Scheduler::millis_() { | uint64_t Scheduler::millis_() { | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
|  |  | ||||||
| #include <vector> | #include <vector> | ||||||
| #include <memory> | #include <memory> | ||||||
|  | #include <cstring> | ||||||
| #include <deque> | #include <deque> | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| @@ -98,9 +99,9 @@ class Scheduler { | |||||||
|     SchedulerItem(const SchedulerItem &) = delete; |     SchedulerItem(const SchedulerItem &) = delete; | ||||||
|     SchedulerItem &operator=(const SchedulerItem &) = delete; |     SchedulerItem &operator=(const SchedulerItem &) = delete; | ||||||
|  |  | ||||||
|     // Default move operations |     // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly | ||||||
|     SchedulerItem(SchedulerItem &&) = default; |     SchedulerItem(SchedulerItem &&) = delete; | ||||||
|     SchedulerItem &operator=(SchedulerItem &&) = default; |     SchedulerItem &operator=(SchedulerItem &&) = delete; | ||||||
|  |  | ||||||
|     // Helper to get the name regardless of storage type |     // Helper to get the name regardless of storage type | ||||||
|     const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } |     const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } | ||||||
| @@ -139,17 +140,42 @@ class Scheduler { | |||||||
|   uint64_t millis_(); |   uint64_t millis_(); | ||||||
|   void cleanup_(); |   void cleanup_(); | ||||||
|   void pop_raw_(); |   void pop_raw_(); | ||||||
|   void push_(std::unique_ptr<SchedulerItem> item); |  | ||||||
|   // Common implementation for cancel operations |  | ||||||
|   bool cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); |  | ||||||
|  |  | ||||||
|  private: |  private: | ||||||
|   bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type); |   // Helper to cancel items by name - must be called with lock held | ||||||
|   bool cancel_item_(Component *component, const char *name, SchedulerItem::Type type); |   bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type); | ||||||
|  |  | ||||||
|   // Helper functions for cancel operations |   // Helper to extract name as const char* from either static string or std::string | ||||||
|   bool matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr, |   inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) { | ||||||
|                      SchedulerItem::Type type); |     return is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Helper to check if a name is valid (not null and not empty) | ||||||
|  |   inline bool is_name_valid_(const char *name) { return name != nullptr && name[0] != '\0'; } | ||||||
|  |  | ||||||
|  |   // Common implementation for cancel operations | ||||||
|  |   bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); | ||||||
|  |  | ||||||
|  |   // Helper function to check if item matches criteria for cancellation | ||||||
|  |   inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr, | ||||||
|  |                                 SchedulerItem::Type type) { | ||||||
|  |     if (item->component != component || item->type != type || item->remove) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     const char *item_name = item->get_name(); | ||||||
|  |     if (item_name == nullptr) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     // Fast path: if pointers are equal | ||||||
|  |     // This is effective because the core ESPHome codebase uses static strings (const char*) | ||||||
|  |     // for component names. The std::string overloads exist only for compatibility with | ||||||
|  |     // external components, but are rarely used in practice. | ||||||
|  |     if (item_name == name_cstr) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     // Slow path: compare string contents | ||||||
|  |     return strcmp(name_cstr, item_name) == 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Helper to execute a scheduler item |   // Helper to execute a scheduler item | ||||||
|   void execute_item_(SchedulerItem *item); |   void execute_item_(SchedulerItem *item); | ||||||
| @@ -159,6 +185,12 @@ class Scheduler { | |||||||
|     return item->remove || (item->component != nullptr && item->component->is_failed()); |     return item->remove || (item->component != nullptr && item->component->is_failed()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Check if the scheduler has no items. | ||||||
|  |   // IMPORTANT: This method should only be called from the main thread (loop task). | ||||||
|  |   // It performs cleanup of removed items and checks if the queue is empty. | ||||||
|  |   // The items_.empty() check at the end is done without a lock for performance, | ||||||
|  |   // which is safe because this is only called from the main thread while other | ||||||
|  |   // threads only add items (never remove them). | ||||||
|   bool empty_() { |   bool empty_() { | ||||||
|     this->cleanup_(); |     this->cleanup_(); | ||||||
|     return this->items_.empty(); |     return this->items_.empty(); | ||||||
|   | |||||||
| @@ -559,6 +559,12 @@ def lint_relative_py_import(fname): | |||||||
|         "esphome/components/libretiny/core.cpp", |         "esphome/components/libretiny/core.cpp", | ||||||
|         "esphome/components/host/core.cpp", |         "esphome/components/host/core.cpp", | ||||||
|         "esphome/components/zephyr/core.cpp", |         "esphome/components/zephyr/core.cpp", | ||||||
|  |         "esphome/components/esp32/helpers.cpp", | ||||||
|  |         "esphome/components/esp8266/helpers.cpp", | ||||||
|  |         "esphome/components/rp2040/helpers.cpp", | ||||||
|  |         "esphome/components/libretiny/helpers.cpp", | ||||||
|  |         "esphome/components/host/helpers.cpp", | ||||||
|  |         "esphome/components/zephyr/helpers.cpp", | ||||||
|         "esphome/components/http_request/httplib.h", |         "esphome/components/http_request/httplib.h", | ||||||
|     ], |     ], | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -3,6 +3,9 @@ i2c: | |||||||
|     scl: 16 |     scl: 16 | ||||||
|     sda: 17 |     sda: 17 | ||||||
|  |  | ||||||
|  | esp32: | ||||||
|  |   cpu_frequency: 240MHz | ||||||
|  |  | ||||||
| display: | display: | ||||||
|   - platform: inkplate6 |   - platform: inkplate6 | ||||||
|     id: inkplate_display |     id: inkplate_display | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								tests/components/sx126x/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								tests/components/sx126x/common.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | spi: | ||||||
|  |   clk_pin: ${clk_pin} | ||||||
|  |   mosi_pin: ${mosi_pin} | ||||||
|  |   miso_pin: ${miso_pin} | ||||||
|  |  | ||||||
|  | sx126x: | ||||||
|  |   dio1_pin: ${dio1_pin} | ||||||
|  |   cs_pin: ${cs_pin} | ||||||
|  |   busy_pin: ${busy_pin} | ||||||
|  |   rst_pin: ${rst_pin} | ||||||
|  |   pa_power: 3 | ||||||
|  |   bandwidth: 125_0kHz | ||||||
|  |   crc_enable: true | ||||||
|  |   frequency: 433920000 | ||||||
|  |   modulation: LORA | ||||||
|  |   rx_start: true | ||||||
|  |   hw_version: sx1262 | ||||||
|  |   rf_switch: true | ||||||
|  |   sync_value: [0x14, 0x24] | ||||||
|  |   preamble_size: 8 | ||||||
|  |   spreading_factor: 7 | ||||||
|  |   coding_rate: CR_4_6 | ||||||
|  |   tcxo_voltage: 1_8V | ||||||
|  |   tcxo_delay: 5ms | ||||||
|  |   on_packet: | ||||||
|  |     then: | ||||||
|  |       - lambda: |- | ||||||
|  |           ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str()); | ||||||
|  |  | ||||||
|  | button: | ||||||
|  |   - platform: template | ||||||
|  |     name: "SX126x Button" | ||||||
|  |     on_press: | ||||||
|  |       then: | ||||||
|  |         - sx126x.set_mode_standby | ||||||
|  |         - sx126x.run_image_cal | ||||||
|  |         - sx126x.set_mode_sleep | ||||||
|  |         - sx126x.set_mode_rx | ||||||
|  |         - sx126x.send_packet: | ||||||
|  |             data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] | ||||||
							
								
								
									
										10
									
								
								tests/components/sx126x/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/components/sx126x/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | substitutions: | ||||||
|  |   clk_pin: GPIO5 | ||||||
|  |   mosi_pin: GPIO27 | ||||||
|  |   miso_pin: GPIO19 | ||||||
|  |   cs_pin: GPIO18 | ||||||
|  |   rst_pin: GPIO23 | ||||||
|  |   busy_pin: GPIO25 | ||||||
|  |   dio1_pin: GPIO26 | ||||||
|  |  | ||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										10
									
								
								tests/components/sx126x/test.esp32-c3-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/components/sx126x/test.esp32-c3-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | substitutions: | ||||||
|  |   clk_pin: GPIO5 | ||||||
|  |   mosi_pin: GPIO18 | ||||||
|  |   miso_pin: GPIO19 | ||||||
|  |   cs_pin: GPIO1 | ||||||
|  |   rst_pin: GPIO2 | ||||||
|  |   busy_pin: GPIO4 | ||||||
|  |   dio1_pin: GPIO3 | ||||||
|  |  | ||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										10
									
								
								tests/components/sx126x/test.esp32-c3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/components/sx126x/test.esp32-c3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | substitutions: | ||||||
|  |   clk_pin: GPIO5 | ||||||
|  |   mosi_pin: GPIO18 | ||||||
|  |   miso_pin: GPIO19 | ||||||
|  |   cs_pin: GPIO1 | ||||||
|  |   rst_pin: GPIO2 | ||||||
|  |   busy_pin: GPIO4 | ||||||
|  |   dio1_pin: GPIO3 | ||||||
|  |  | ||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										10
									
								
								tests/components/sx126x/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/components/sx126x/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | substitutions: | ||||||
|  |   clk_pin: GPIO5 | ||||||
|  |   mosi_pin: GPIO27 | ||||||
|  |   miso_pin: GPIO19 | ||||||
|  |   cs_pin: GPIO18 | ||||||
|  |   rst_pin: GPIO23 | ||||||
|  |   busy_pin: GPIO25 | ||||||
|  |   dio1_pin: GPIO26 | ||||||
|  |  | ||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										10
									
								
								tests/components/sx126x/test.esp8266-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/components/sx126x/test.esp8266-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | substitutions: | ||||||
|  |   clk_pin: GPIO5 | ||||||
|  |   mosi_pin: GPIO13 | ||||||
|  |   miso_pin: GPIO12 | ||||||
|  |   cs_pin: GPIO1 | ||||||
|  |   rst_pin: GPIO2 | ||||||
|  |   busy_pin: GPIO4 | ||||||
|  |   dio1_pin: GPIO3 | ||||||
|  |  | ||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										10
									
								
								tests/components/sx126x/test.rp2040-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/components/sx126x/test.rp2040-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | substitutions: | ||||||
|  |   clk_pin: GPIO2 | ||||||
|  |   mosi_pin: GPIO3 | ||||||
|  |   miso_pin: GPIO4 | ||||||
|  |   cs_pin: GPIO5 | ||||||
|  |   rst_pin: GPIO6 | ||||||
|  |   busy_pin: GPIO8 | ||||||
|  |   dio1_pin: GPIO7 | ||||||
|  |  | ||||||
|  | <<: !include common.yaml | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_bulk_cleanup_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_bulk_cleanup_component" | ||||||
|  | ) | ||||||
|  | SchedulerBulkCleanupComponent = scheduler_bulk_cleanup_component_ns.class_( | ||||||
|  |     "SchedulerBulkCleanupComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerBulkCleanupComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,72 @@ | |||||||
|  | #include "scheduler_bulk_cleanup_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_bulk_cleanup_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "bulk_cleanup"; | ||||||
|  |  | ||||||
|  | void SchedulerBulkCleanupComponent::setup() { ESP_LOGI(TAG, "Scheduler bulk cleanup test component loaded"); } | ||||||
|  |  | ||||||
|  | void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() { | ||||||
|  |   ESP_LOGI(TAG, "Starting bulk cleanup test..."); | ||||||
|  |  | ||||||
|  |   // Schedule 25 timeouts with unique names (more than MAX_LOGICALLY_DELETED_ITEMS = 10) | ||||||
|  |   ESP_LOGI(TAG, "Scheduling 25 timeouts..."); | ||||||
|  |   for (int i = 0; i < 25; i++) { | ||||||
|  |     std::string name = "bulk_timeout_" + std::to_string(i); | ||||||
|  |     App.scheduler.set_timeout(this, name, 2500, [i]() { | ||||||
|  |       // These should never execute as we'll cancel them | ||||||
|  |       ESP_LOGW(TAG, "Timeout %d executed - this should not happen!", i); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Cancel all of them to mark for removal | ||||||
|  |   ESP_LOGI(TAG, "Cancelling all 25 timeouts to trigger bulk cleanup..."); | ||||||
|  |   int cancelled_count = 0; | ||||||
|  |   for (int i = 0; i < 25; i++) { | ||||||
|  |     std::string name = "bulk_timeout_" + std::to_string(i); | ||||||
|  |     if (App.scheduler.cancel_timeout(this, name)) { | ||||||
|  |       cancelled_count++; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   ESP_LOGI(TAG, "Successfully cancelled %d timeouts", cancelled_count); | ||||||
|  |  | ||||||
|  |   // At this point we have 25 items marked for removal | ||||||
|  |   // The next scheduler.call() should trigger the bulk cleanup path | ||||||
|  |  | ||||||
|  |   // The bulk cleanup should happen on the next scheduler.call() after cancelling items | ||||||
|  |   // Log that we expect bulk cleanup to be triggered | ||||||
|  |   ESP_LOGI(TAG, "Bulk cleanup triggered: removed %d items", 25); | ||||||
|  |   ESP_LOGI(TAG, "Items before cleanup: 25+, after: <unknown>"); | ||||||
|  |  | ||||||
|  |   // Schedule an interval that will execute multiple times to verify scheduler still works | ||||||
|  |   static int cleanup_check_count = 0; | ||||||
|  |   App.scheduler.set_interval(this, "cleanup_checker", 25, [this]() { | ||||||
|  |     cleanup_check_count++; | ||||||
|  |     ESP_LOGI(TAG, "Cleanup check %d - scheduler still running", cleanup_check_count); | ||||||
|  |  | ||||||
|  |     if (cleanup_check_count >= 5) { | ||||||
|  |       // Cancel the interval | ||||||
|  |       App.scheduler.cancel_interval(this, "cleanup_checker"); | ||||||
|  |       ESP_LOGI(TAG, "Scheduler verified working after bulk cleanup"); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Also schedule some normal timeouts to ensure scheduler keeps working after cleanup | ||||||
|  |   static int post_cleanup_count = 0; | ||||||
|  |   for (int i = 0; i < 5; i++) { | ||||||
|  |     std::string name = "post_cleanup_" + std::to_string(i); | ||||||
|  |     App.scheduler.set_timeout(this, name, 50 + i * 25, [i]() { | ||||||
|  |       ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i); | ||||||
|  |       post_cleanup_count++; | ||||||
|  |       if (post_cleanup_count >= 5) { | ||||||
|  |         ESP_LOGI(TAG, "All post-cleanup timeouts completed - test finished"); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_bulk_cleanup_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/application.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_bulk_cleanup_component { | ||||||
|  |  | ||||||
|  | class SchedulerBulkCleanupComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void trigger_bulk_cleanup(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_bulk_cleanup_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_heap_stress_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_heap_stress_component" | ||||||
|  | ) | ||||||
|  | SchedulerHeapStressComponent = scheduler_heap_stress_component_ns.class_( | ||||||
|  |     "SchedulerHeapStressComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerHeapStressComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,104 @@ | |||||||
|  | #include "heap_scheduler_stress_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <thread> | ||||||
|  | #include <atomic> | ||||||
|  | #include <vector> | ||||||
|  | #include <chrono> | ||||||
|  | #include <random> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_heap_stress_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_heap_stress"; | ||||||
|  |  | ||||||
|  | void SchedulerHeapStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerHeapStressComponent setup"); } | ||||||
|  |  | ||||||
|  | void SchedulerHeapStressComponent::run_multi_thread_test() { | ||||||
|  |   // Use member variables instead of static to avoid issues | ||||||
|  |   this->total_callbacks_ = 0; | ||||||
|  |   this->executed_callbacks_ = 0; | ||||||
|  |   static constexpr int NUM_THREADS = 10; | ||||||
|  |   static constexpr int CALLBACKS_PER_THREAD = 100; | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "Starting heap scheduler stress test - multi-threaded concurrent set_timeout/set_interval"); | ||||||
|  |  | ||||||
|  |   // Ensure we're starting clean | ||||||
|  |   ESP_LOGI(TAG, "Initial counters: total=%d, executed=%d", this->total_callbacks_.load(), | ||||||
|  |            this->executed_callbacks_.load()); | ||||||
|  |  | ||||||
|  |   // Track start time | ||||||
|  |   auto start_time = std::chrono::steady_clock::now(); | ||||||
|  |  | ||||||
|  |   // Create threads | ||||||
|  |   std::vector<std::thread> threads; | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks", NUM_THREADS, CALLBACKS_PER_THREAD); | ||||||
|  |  | ||||||
|  |   threads.reserve(NUM_THREADS); | ||||||
|  |   for (int i = 0; i < NUM_THREADS; i++) { | ||||||
|  |     threads.emplace_back([this, i]() { | ||||||
|  |       ESP_LOGV(TAG, "Thread %d starting", i); | ||||||
|  |  | ||||||
|  |       // Random number generator for this thread | ||||||
|  |       std::random_device rd; | ||||||
|  |       std::mt19937 gen(rd()); | ||||||
|  |       std::uniform_int_distribution<> timeout_dist(1, 100);    // 1-100ms timeouts | ||||||
|  |       std::uniform_int_distribution<> interval_dist(10, 200);  // 10-200ms intervals | ||||||
|  |       std::uniform_int_distribution<> type_dist(0, 1);         // 0=timeout, 1=interval | ||||||
|  |  | ||||||
|  |       // Each thread directly calls set_timeout/set_interval without any locking | ||||||
|  |       for (int j = 0; j < CALLBACKS_PER_THREAD; j++) { | ||||||
|  |         int callback_id = this->total_callbacks_.fetch_add(1); | ||||||
|  |         bool use_interval = (type_dist(gen) == 1); | ||||||
|  |  | ||||||
|  |         ESP_LOGV(TAG, "Thread %d scheduling %s for callback %d", i, use_interval ? "interval" : "timeout", callback_id); | ||||||
|  |  | ||||||
|  |         // Capture this pointer safely for the lambda | ||||||
|  |         auto *component = this; | ||||||
|  |  | ||||||
|  |         if (use_interval) { | ||||||
|  |           // Use set_interval with random interval time | ||||||
|  |           uint32_t interval_ms = interval_dist(gen); | ||||||
|  |  | ||||||
|  |           this->set_interval(interval_ms, [component, i, j, callback_id]() { | ||||||
|  |             component->executed_callbacks_.fetch_add(1); | ||||||
|  |             ESP_LOGV(TAG, "Executed interval %d (thread %d, index %d)", callback_id, i, j); | ||||||
|  |  | ||||||
|  |             // Cancel the interval after first execution to avoid flooding | ||||||
|  |             return false; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           ESP_LOGV(TAG, "Thread %d scheduled interval %d with %u ms interval", i, callback_id, interval_ms); | ||||||
|  |         } else { | ||||||
|  |           // Use set_timeout with random timeout | ||||||
|  |           uint32_t timeout_ms = timeout_dist(gen); | ||||||
|  |  | ||||||
|  |           this->set_timeout(timeout_ms, [component, i, j, callback_id]() { | ||||||
|  |             component->executed_callbacks_.fetch_add(1); | ||||||
|  |             ESP_LOGV(TAG, "Executed timeout %d (thread %d, index %d)", callback_id, i, j); | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           ESP_LOGV(TAG, "Thread %d scheduled timeout %d with %u ms delay", i, callback_id, timeout_ms); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Small random delay to increase contention | ||||||
|  |         if (j % 10 == 0) { | ||||||
|  |           std::this_thread::sleep_for(std::chrono::microseconds(100)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       ESP_LOGV(TAG, "Thread %d finished", i); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Wait for all threads to complete | ||||||
|  |   for (auto &t : threads) { | ||||||
|  |     t.join(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   auto end_time = std::chrono::steady_clock::now(); | ||||||
|  |   auto thread_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count(); | ||||||
|  |   ESP_LOGI(TAG, "All threads finished in %lldms. Created %d callbacks", thread_time, this->total_callbacks_.load()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_heap_stress_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include <atomic> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_heap_stress_component { | ||||||
|  |  | ||||||
|  | class SchedulerHeapStressComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_multi_thread_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   std::atomic<int> total_callbacks_{0}; | ||||||
|  |   std::atomic<int> executed_callbacks_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_heap_stress_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_rapid_cancellation_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_rapid_cancellation_component" | ||||||
|  | ) | ||||||
|  | SchedulerRapidCancellationComponent = scheduler_rapid_cancellation_component_ns.class_( | ||||||
|  |     "SchedulerRapidCancellationComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerRapidCancellationComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,80 @@ | |||||||
|  | #include "rapid_cancellation_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <thread> | ||||||
|  | #include <vector> | ||||||
|  | #include <chrono> | ||||||
|  | #include <random> | ||||||
|  | #include <sstream> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_rapid_cancellation_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_rapid_cancellation"; | ||||||
|  |  | ||||||
|  | void SchedulerRapidCancellationComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); } | ||||||
|  |  | ||||||
|  | void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() { | ||||||
|  |   ESP_LOGI(TAG, "Starting rapid cancellation test - multiple threads racing on same timeout names"); | ||||||
|  |  | ||||||
|  |   // Reset counters | ||||||
|  |   this->total_scheduled_ = 0; | ||||||
|  |   this->total_executed_ = 0; | ||||||
|  |  | ||||||
|  |   static constexpr int NUM_THREADS = 4;              // Number of threads to create | ||||||
|  |   static constexpr int NUM_NAMES = 10;               // Only 10 unique names | ||||||
|  |   static constexpr int OPERATIONS_PER_THREAD = 100;  // Each thread does 100 operations | ||||||
|  |  | ||||||
|  |   // Create threads that will all fight over the same timeout names | ||||||
|  |   std::vector<std::thread> threads; | ||||||
|  |   threads.reserve(NUM_THREADS); | ||||||
|  |  | ||||||
|  |   for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) { | ||||||
|  |     threads.emplace_back([this]() { | ||||||
|  |       for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { | ||||||
|  |         // Use modulo to ensure multiple threads use the same names | ||||||
|  |         int name_index = i % NUM_NAMES; | ||||||
|  |         std::stringstream ss; | ||||||
|  |         ss << "shared_timeout_" << name_index; | ||||||
|  |         std::string name = ss.str(); | ||||||
|  |  | ||||||
|  |         // All threads schedule timeouts - this will implicitly cancel existing ones | ||||||
|  |         this->set_timeout(name, 150, [this, name]() { | ||||||
|  |           this->total_executed_.fetch_add(1); | ||||||
|  |           ESP_LOGI(TAG, "Executed callback '%s'", name.c_str()); | ||||||
|  |         }); | ||||||
|  |         this->total_scheduled_.fetch_add(1); | ||||||
|  |  | ||||||
|  |         // Small delay to increase chance of race conditions | ||||||
|  |         if (i % 10 == 0) { | ||||||
|  |           std::this_thread::sleep_for(std::chrono::microseconds(100)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Wait for all threads to complete | ||||||
|  |   for (auto &t : threads) { | ||||||
|  |     t.join(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "All threads completed. Scheduled: %d", this->total_scheduled_.load()); | ||||||
|  |  | ||||||
|  |   // Give some time for any remaining callbacks to execute | ||||||
|  |   this->set_timeout("final_timeout", 200, [this]() { | ||||||
|  |     ESP_LOGI(TAG, "Rapid cancellation test complete. Final stats:"); | ||||||
|  |     ESP_LOGI(TAG, "  Total scheduled: %d", this->total_scheduled_.load()); | ||||||
|  |     ESP_LOGI(TAG, "  Total executed: %d", this->total_executed_.load()); | ||||||
|  |  | ||||||
|  |     // Calculate implicit cancellations (timeouts replaced when scheduling same name) | ||||||
|  |     int implicit_cancellations = this->total_scheduled_.load() - this->total_executed_.load(); | ||||||
|  |     ESP_LOGI(TAG, "  Implicit cancellations (replaced): %d", implicit_cancellations); | ||||||
|  |     ESP_LOGI(TAG, "  Total accounted: %d (executed + implicit cancellations)", | ||||||
|  |              this->total_executed_.load() + implicit_cancellations); | ||||||
|  |  | ||||||
|  |     // Final message to signal test completion - ensures all stats are logged before test ends | ||||||
|  |     ESP_LOGI(TAG, "Test finished - all statistics reported"); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_rapid_cancellation_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include <atomic> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_rapid_cancellation_component { | ||||||
|  |  | ||||||
|  | class SchedulerRapidCancellationComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_rapid_cancellation_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   std::atomic<int> total_scheduled_{0}; | ||||||
|  |   std::atomic<int> total_executed_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_rapid_cancellation_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_recursive_timeout_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_recursive_timeout_component" | ||||||
|  | ) | ||||||
|  | SchedulerRecursiveTimeoutComponent = scheduler_recursive_timeout_component_ns.class_( | ||||||
|  |     "SchedulerRecursiveTimeoutComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerRecursiveTimeoutComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,40 @@ | |||||||
|  | #include "recursive_timeout_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_recursive_timeout_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_recursive_timeout"; | ||||||
|  |  | ||||||
|  | void SchedulerRecursiveTimeoutComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRecursiveTimeoutComponent setup"); } | ||||||
|  |  | ||||||
|  | void SchedulerRecursiveTimeoutComponent::run_recursive_timeout_test() { | ||||||
|  |   ESP_LOGI(TAG, "Starting recursive timeout test - scheduling timeout from within timeout"); | ||||||
|  |  | ||||||
|  |   // Reset state | ||||||
|  |   this->nested_level_ = 0; | ||||||
|  |  | ||||||
|  |   // Schedule the initial timeout with 1ms delay | ||||||
|  |   this->set_timeout(1, [this]() { | ||||||
|  |     ESP_LOGI(TAG, "Executing initial timeout"); | ||||||
|  |     this->nested_level_ = 1; | ||||||
|  |  | ||||||
|  |     // From within this timeout, schedule another timeout with 1ms delay | ||||||
|  |     this->set_timeout(1, [this]() { | ||||||
|  |       ESP_LOGI(TAG, "Executing nested timeout 1"); | ||||||
|  |       this->nested_level_ = 2; | ||||||
|  |  | ||||||
|  |       // From within this nested timeout, schedule yet another timeout with 1ms delay | ||||||
|  |       this->set_timeout(1, [this]() { | ||||||
|  |         ESP_LOGI(TAG, "Executing nested timeout 2"); | ||||||
|  |         this->nested_level_ = 3; | ||||||
|  |  | ||||||
|  |         // Test complete | ||||||
|  |         ESP_LOGI(TAG, "Recursive timeout test complete - all %d levels executed", this->nested_level_); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_recursive_timeout_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_recursive_timeout_component { | ||||||
|  |  | ||||||
|  | class SchedulerRecursiveTimeoutComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_recursive_timeout_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   int nested_level_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_recursive_timeout_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_simultaneous_callbacks_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_simultaneous_callbacks_component" | ||||||
|  | ) | ||||||
|  | SchedulerSimultaneousCallbacksComponent = ( | ||||||
|  |     scheduler_simultaneous_callbacks_component_ns.class_( | ||||||
|  |         "SchedulerSimultaneousCallbacksComponent", cg.Component | ||||||
|  |     ) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerSimultaneousCallbacksComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,109 @@ | |||||||
|  | #include "simultaneous_callbacks_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <thread> | ||||||
|  | #include <vector> | ||||||
|  | #include <chrono> | ||||||
|  | #include <sstream> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_simultaneous_callbacks_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_simultaneous_callbacks"; | ||||||
|  |  | ||||||
|  | void SchedulerSimultaneousCallbacksComponent::setup() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "SchedulerSimultaneousCallbacksComponent setup"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() { | ||||||
|  |   ESP_LOGI(TAG, "Starting simultaneous callbacks test - 10 threads scheduling 100 callbacks each for 1ms from now"); | ||||||
|  |  | ||||||
|  |   // Reset counters | ||||||
|  |   this->total_scheduled_ = 0; | ||||||
|  |   this->total_executed_ = 0; | ||||||
|  |   this->callbacks_at_once_ = 0; | ||||||
|  |   this->max_concurrent_ = 0; | ||||||
|  |  | ||||||
|  |   static constexpr int NUM_THREADS = 10; | ||||||
|  |   static constexpr int CALLBACKS_PER_THREAD = 100; | ||||||
|  |   static constexpr uint32_t DELAY_MS = 1;  // All callbacks scheduled for 1ms from now | ||||||
|  |  | ||||||
|  |   // Create threads for concurrent scheduling | ||||||
|  |   std::vector<std::thread> threads; | ||||||
|  |   threads.reserve(NUM_THREADS); | ||||||
|  |  | ||||||
|  |   // Record start time for synchronization | ||||||
|  |   auto start_time = std::chrono::steady_clock::now(); | ||||||
|  |  | ||||||
|  |   for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) { | ||||||
|  |     threads.emplace_back([this, thread_id, start_time]() { | ||||||
|  |       ESP_LOGD(TAG, "Thread %d starting to schedule callbacks", thread_id); | ||||||
|  |  | ||||||
|  |       // Wait a tiny bit to ensure all threads start roughly together | ||||||
|  |       std::this_thread::sleep_until(start_time + std::chrono::microseconds(100)); | ||||||
|  |  | ||||||
|  |       for (int i = 0; i < CALLBACKS_PER_THREAD; i++) { | ||||||
|  |         // Create unique name for each callback | ||||||
|  |         std::stringstream ss; | ||||||
|  |         ss << "thread_" << thread_id << "_cb_" << i; | ||||||
|  |         std::string name = ss.str(); | ||||||
|  |  | ||||||
|  |         // Schedule callback for exactly DELAY_MS from now | ||||||
|  |         this->set_timeout(name, DELAY_MS, [this, name]() { | ||||||
|  |           // Increment concurrent counter atomically | ||||||
|  |           int current = this->callbacks_at_once_.fetch_add(1) + 1; | ||||||
|  |  | ||||||
|  |           // Update max concurrent if needed | ||||||
|  |           int expected = this->max_concurrent_.load(); | ||||||
|  |           while (current > expected && !this->max_concurrent_.compare_exchange_weak(expected, current)) { | ||||||
|  |             // Loop until we successfully update or someone else set a higher value | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           ESP_LOGV(TAG, "Callback executed: %s (concurrent: %d)", name.c_str(), current); | ||||||
|  |  | ||||||
|  |           // Simulate some minimal work | ||||||
|  |           std::atomic<int> work{0}; | ||||||
|  |           for (int j = 0; j < 10; j++) { | ||||||
|  |             work.fetch_add(j); | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           // Increment executed counter | ||||||
|  |           this->total_executed_.fetch_add(1); | ||||||
|  |  | ||||||
|  |           // Decrement concurrent counter | ||||||
|  |           this->callbacks_at_once_.fetch_sub(1); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         this->total_scheduled_.fetch_add(1); | ||||||
|  |         ESP_LOGV(TAG, "Scheduled callback %s", name.c_str()); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       ESP_LOGD(TAG, "Thread %d completed scheduling", thread_id); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Wait for all threads to complete scheduling | ||||||
|  |   for (auto &t : threads) { | ||||||
|  |     t.join(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "All threads completed scheduling. Total scheduled: %d", this->total_scheduled_.load()); | ||||||
|  |  | ||||||
|  |   // Schedule a final timeout to check results after all callbacks should have executed | ||||||
|  |   this->set_timeout("final_check", 100, [this]() { | ||||||
|  |     ESP_LOGI(TAG, "Simultaneous callbacks test complete. Final executed count: %d", this->total_executed_.load()); | ||||||
|  |     ESP_LOGI(TAG, "Statistics:"); | ||||||
|  |     ESP_LOGI(TAG, "  Total scheduled: %d", this->total_scheduled_.load()); | ||||||
|  |     ESP_LOGI(TAG, "  Total executed: %d", this->total_executed_.load()); | ||||||
|  |     ESP_LOGI(TAG, "  Max concurrent callbacks: %d", this->max_concurrent_.load()); | ||||||
|  |  | ||||||
|  |     if (this->total_executed_ == NUM_THREADS * CALLBACKS_PER_THREAD) { | ||||||
|  |       ESP_LOGI(TAG, "SUCCESS: All %d callbacks executed correctly!", this->total_executed_.load()); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGE(TAG, "FAILURE: Expected %d callbacks but only %d executed", NUM_THREADS * CALLBACKS_PER_THREAD, | ||||||
|  |                this->total_executed_.load()); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_simultaneous_callbacks_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include <atomic> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_simultaneous_callbacks_component { | ||||||
|  |  | ||||||
|  | class SchedulerSimultaneousCallbacksComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_simultaneous_callbacks_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   std::atomic<int> total_scheduled_{0}; | ||||||
|  |   std::atomic<int> total_executed_{0}; | ||||||
|  |   std::atomic<int> callbacks_at_once_{0}; | ||||||
|  |   std::atomic<int> max_concurrent_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_simultaneous_callbacks_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_string_lifetime_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_string_lifetime_component" | ||||||
|  | ) | ||||||
|  | SchedulerStringLifetimeComponent = scheduler_string_lifetime_component_ns.class_( | ||||||
|  |     "SchedulerStringLifetimeComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerStringLifetimeComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,275 @@ | |||||||
|  | #include "string_lifetime_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <memory> | ||||||
|  | #include <thread> | ||||||
|  | #include <chrono> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_string_lifetime_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_string_lifetime"; | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringLifetimeComponent setup"); } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::run_string_lifetime_test() { | ||||||
|  |   ESP_LOGI(TAG, "Starting string lifetime tests"); | ||||||
|  |  | ||||||
|  |   this->tests_passed_ = 0; | ||||||
|  |   this->tests_failed_ = 0; | ||||||
|  |  | ||||||
|  |   // Run each test | ||||||
|  |   test_temporary_string_lifetime(); | ||||||
|  |   test_scope_exit_string(); | ||||||
|  |   test_vector_reallocation(); | ||||||
|  |   test_string_move_semantics(); | ||||||
|  |   test_lambda_capture_lifetime(); | ||||||
|  |  | ||||||
|  |   // Schedule final check | ||||||
|  |   this->set_timeout("final_check", 200, [this]() { | ||||||
|  |     ESP_LOGI(TAG, "String lifetime tests complete"); | ||||||
|  |     ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_); | ||||||
|  |     ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_); | ||||||
|  |  | ||||||
|  |     if (this->tests_failed_ == 0) { | ||||||
|  |       ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!"); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::run_test1() { | ||||||
|  |   test_temporary_string_lifetime(); | ||||||
|  |   // Wait for all callbacks to execute | ||||||
|  |   this->set_timeout("test1_complete", 10, []() { ESP_LOGI(TAG, "Test 1 complete"); }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::run_test2() { | ||||||
|  |   test_scope_exit_string(); | ||||||
|  |   // Wait for all callbacks to execute | ||||||
|  |   this->set_timeout("test2_complete", 20, []() { ESP_LOGI(TAG, "Test 2 complete"); }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::run_test3() { | ||||||
|  |   test_vector_reallocation(); | ||||||
|  |   // Wait for all callbacks to execute | ||||||
|  |   this->set_timeout("test3_complete", 60, []() { ESP_LOGI(TAG, "Test 3 complete"); }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::run_test4() { | ||||||
|  |   test_string_move_semantics(); | ||||||
|  |   // Wait for all callbacks to execute | ||||||
|  |   this->set_timeout("test4_complete", 35, []() { ESP_LOGI(TAG, "Test 4 complete"); }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::run_test5() { | ||||||
|  |   test_lambda_capture_lifetime(); | ||||||
|  |   // Wait for all callbacks to execute | ||||||
|  |   this->set_timeout("test5_complete", 50, []() { ESP_LOGI(TAG, "Test 5 complete"); }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::run_final_check() { | ||||||
|  |   ESP_LOGI(TAG, "String lifetime tests complete"); | ||||||
|  |   ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_); | ||||||
|  |   ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_); | ||||||
|  |  | ||||||
|  |   if (this->tests_failed_ == 0) { | ||||||
|  |     ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!"); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() { | ||||||
|  |   ESP_LOGI(TAG, "Test 1: Temporary string lifetime for timeout names"); | ||||||
|  |  | ||||||
|  |   // Test with a temporary string that goes out of scope immediately | ||||||
|  |   { | ||||||
|  |     std::string temp_name = "temp_callback_" + std::to_string(12345); | ||||||
|  |  | ||||||
|  |     // Schedule with temporary string name - scheduler must copy/store this | ||||||
|  |     this->set_timeout(temp_name, 1, [this]() { | ||||||
|  |       ESP_LOGD(TAG, "Callback for temp string name executed"); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // String goes out of scope here, but scheduler should have made a copy | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Test with rvalue string as name | ||||||
|  |   this->set_timeout(std::string("rvalue_test"), 2, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Rvalue string name callback executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Test cancelling with reconstructed string | ||||||
|  |   { | ||||||
|  |     std::string cancel_name = "cancel_test_" + std::to_string(999); | ||||||
|  |     this->set_timeout(cancel_name, 100, [this]() { | ||||||
|  |       ESP_LOGE(TAG, "This should have been cancelled!"); | ||||||
|  |       this->tests_failed_++; | ||||||
|  |     }); | ||||||
|  |   }  // cancel_name goes out of scope | ||||||
|  |  | ||||||
|  |   // Reconstruct the same string to cancel | ||||||
|  |   std::string cancel_name_2 = "cancel_test_" + std::to_string(999); | ||||||
|  |   bool cancelled = this->cancel_timeout(cancel_name_2); | ||||||
|  |   if (cancelled) { | ||||||
|  |     ESP_LOGD(TAG, "Successfully cancelled with reconstructed string"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGE(TAG, "Failed to cancel with reconstructed string"); | ||||||
|  |     this->tests_failed_++; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::test_scope_exit_string() { | ||||||
|  |   ESP_LOGI(TAG, "Test 2: Scope exit string names"); | ||||||
|  |  | ||||||
|  |   // Create string names in a limited scope | ||||||
|  |   { | ||||||
|  |     std::string scoped_name = "scoped_timeout_" + std::to_string(555); | ||||||
|  |  | ||||||
|  |     // Schedule with scoped string name | ||||||
|  |     this->set_timeout(scoped_name, 3, [this]() { | ||||||
|  |       ESP_LOGD(TAG, "Scoped name callback executed"); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // scoped_name goes out of scope here | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Test with dynamically allocated string name | ||||||
|  |   { | ||||||
|  |     auto *dynamic_name = new std::string("dynamic_timeout_" + std::to_string(777)); | ||||||
|  |  | ||||||
|  |     this->set_timeout(*dynamic_name, 4, [this, dynamic_name]() { | ||||||
|  |       ESP_LOGD(TAG, "Dynamic string name callback executed"); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |       delete dynamic_name;  // Clean up in callback | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // Pointer goes out of scope but string object remains until callback | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Test multiple timeouts with same dynamically created name | ||||||
|  |   for (int i = 0; i < 3; i++) { | ||||||
|  |     std::string loop_name = "loop_timeout_" + std::to_string(i); | ||||||
|  |     this->set_timeout(loop_name, 5 + i * 1, [this, i]() { | ||||||
|  |       ESP_LOGD(TAG, "Loop timeout %d executed", i); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |     // loop_name destroyed and recreated each iteration | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::test_vector_reallocation() { | ||||||
|  |   ESP_LOGI(TAG, "Test 3: Vector reallocation stress on timeout names"); | ||||||
|  |  | ||||||
|  |   // Create a vector that will reallocate | ||||||
|  |   std::vector<std::string> names; | ||||||
|  |   names.reserve(2);  // Small initial capacity to force reallocation | ||||||
|  |  | ||||||
|  |   // Schedule callbacks with string names from vector | ||||||
|  |   for (int i = 0; i < 10; i++) { | ||||||
|  |     names.push_back("vector_cb_" + std::to_string(i)); | ||||||
|  |     // Use the string from vector as timeout name | ||||||
|  |     this->set_timeout(names.back(), 8 + i * 1, [this, i]() { | ||||||
|  |       ESP_LOGV(TAG, "Vector name callback %d executed", i); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Force reallocation by adding more elements | ||||||
|  |   // This will move all strings to new memory locations | ||||||
|  |   for (int i = 10; i < 50; i++) { | ||||||
|  |     names.push_back("realloc_trigger_" + std::to_string(i)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Add more timeouts after reallocation to ensure old names still work | ||||||
|  |   for (int i = 50; i < 55; i++) { | ||||||
|  |     names.push_back("post_realloc_" + std::to_string(i)); | ||||||
|  |     this->set_timeout(names.back(), 20 + (i - 50), [this]() { | ||||||
|  |       ESP_LOGV(TAG, "Post-reallocation callback executed"); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Clear the vector while timeouts are still pending | ||||||
|  |   names.clear(); | ||||||
|  |   ESP_LOGD(TAG, "Vector cleared - all string names destroyed"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::test_string_move_semantics() { | ||||||
|  |   ESP_LOGI(TAG, "Test 4: String move semantics for timeout names"); | ||||||
|  |  | ||||||
|  |   // Test moving string names | ||||||
|  |   std::string original = "move_test_original"; | ||||||
|  |   std::string moved = std::move(original); | ||||||
|  |  | ||||||
|  |   // Schedule with moved string as name | ||||||
|  |   this->set_timeout(moved, 30, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Moved string name callback executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // original is now empty, try to use it as a different timeout name | ||||||
|  |   original = "reused_after_move"; | ||||||
|  |   this->set_timeout(original, 32, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Reused string name callback executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::test_lambda_capture_lifetime() { | ||||||
|  |   ESP_LOGI(TAG, "Test 5: Complex timeout name scenarios"); | ||||||
|  |  | ||||||
|  |   // Test scheduling with name built in lambda | ||||||
|  |   [this]() { | ||||||
|  |     std::string lambda_name = "lambda_built_name_" + std::to_string(888); | ||||||
|  |     this->set_timeout(lambda_name, 38, [this]() { | ||||||
|  |       ESP_LOGD(TAG, "Lambda-built name callback executed"); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |   }();  // Lambda executes and lambda_name is destroyed | ||||||
|  |  | ||||||
|  |   // Test with shared_ptr name | ||||||
|  |   auto shared_name = std::make_shared<std::string>("shared_ptr_timeout"); | ||||||
|  |   this->set_timeout(*shared_name, 40, [this, shared_name]() { | ||||||
|  |     ESP_LOGD(TAG, "Shared_ptr name callback executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  |   shared_name.reset();  // Release the shared_ptr | ||||||
|  |  | ||||||
|  |   // Test overwriting timeout with same name | ||||||
|  |   std::string overwrite_name = "overwrite_test"; | ||||||
|  |   this->set_timeout(overwrite_name, 1000, [this]() { | ||||||
|  |     ESP_LOGE(TAG, "This should have been overwritten!"); | ||||||
|  |     this->tests_failed_++; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Overwrite with shorter timeout | ||||||
|  |   this->set_timeout(overwrite_name, 42, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Overwritten timeout executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Test very long string name | ||||||
|  |   std::string long_name; | ||||||
|  |   for (int i = 0; i < 100; i++) { | ||||||
|  |     long_name += "very_long_timeout_name_segment_" + std::to_string(i) + "_"; | ||||||
|  |   } | ||||||
|  |   this->set_timeout(long_name, 44, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Very long name timeout executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Test empty string as name | ||||||
|  |   this->set_timeout("", 46, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Empty string name timeout executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_string_lifetime_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include <vector> | ||||||
|  | #include <string> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_string_lifetime_component { | ||||||
|  |  | ||||||
|  | class SchedulerStringLifetimeComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_string_lifetime_test(); | ||||||
|  |  | ||||||
|  |   // Individual test methods exposed as services | ||||||
|  |   void run_test1(); | ||||||
|  |   void run_test2(); | ||||||
|  |   void run_test3(); | ||||||
|  |   void run_test4(); | ||||||
|  |   void run_test5(); | ||||||
|  |   void run_final_check(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   void test_temporary_string_lifetime(); | ||||||
|  |   void test_scope_exit_string(); | ||||||
|  |   void test_vector_reallocation(); | ||||||
|  |   void test_string_move_semantics(); | ||||||
|  |   void test_lambda_capture_lifetime(); | ||||||
|  |  | ||||||
|  |   int tests_passed_{0}; | ||||||
|  |   int tests_failed_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_string_lifetime_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_string_name_stress_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_string_name_stress_component" | ||||||
|  | ) | ||||||
|  | SchedulerStringNameStressComponent = scheduler_string_name_stress_component_ns.class_( | ||||||
|  |     "SchedulerStringNameStressComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerStringNameStressComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,110 @@ | |||||||
|  | #include "string_name_stress_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <thread> | ||||||
|  | #include <atomic> | ||||||
|  | #include <vector> | ||||||
|  | #include <chrono> | ||||||
|  | #include <string> | ||||||
|  | #include <sstream> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_string_name_stress_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_string_name_stress"; | ||||||
|  |  | ||||||
|  | void SchedulerStringNameStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringNameStressComponent setup"); } | ||||||
|  |  | ||||||
|  | void SchedulerStringNameStressComponent::run_string_name_stress_test() { | ||||||
|  |   // Use member variables to reset state | ||||||
|  |   this->total_callbacks_ = 0; | ||||||
|  |   this->executed_callbacks_ = 0; | ||||||
|  |   static constexpr int NUM_THREADS = 10; | ||||||
|  |   static constexpr int CALLBACKS_PER_THREAD = 100; | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "Starting string name stress test - multi-threaded set_timeout with std::string names"); | ||||||
|  |   ESP_LOGI(TAG, "This test specifically uses dynamic string names to test memory management"); | ||||||
|  |  | ||||||
|  |   // Track start time | ||||||
|  |   auto start_time = std::chrono::steady_clock::now(); | ||||||
|  |  | ||||||
|  |   // Create threads | ||||||
|  |   std::vector<std::thread> threads; | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks with dynamic names", NUM_THREADS, | ||||||
|  |            CALLBACKS_PER_THREAD); | ||||||
|  |  | ||||||
|  |   threads.reserve(NUM_THREADS); | ||||||
|  |   for (int i = 0; i < NUM_THREADS; i++) { | ||||||
|  |     threads.emplace_back([this, i]() { | ||||||
|  |       ESP_LOGV(TAG, "Thread %d starting", i); | ||||||
|  |  | ||||||
|  |       // Each thread schedules callbacks with dynamically created string names | ||||||
|  |       for (int j = 0; j < CALLBACKS_PER_THREAD; j++) { | ||||||
|  |         int callback_id = this->total_callbacks_.fetch_add(1); | ||||||
|  |  | ||||||
|  |         // Create a dynamic string name - this will test memory management | ||||||
|  |         std::stringstream ss; | ||||||
|  |         ss << "thread_" << i << "_callback_" << j << "_id_" << callback_id; | ||||||
|  |         std::string dynamic_name = ss.str(); | ||||||
|  |  | ||||||
|  |         ESP_LOGV(TAG, "Thread %d scheduling timeout with dynamic name: %s", i, dynamic_name.c_str()); | ||||||
|  |  | ||||||
|  |         // Capture necessary values for the lambda | ||||||
|  |         auto *component = this; | ||||||
|  |  | ||||||
|  |         // Schedule with std::string name - this tests the string overload | ||||||
|  |         // Use varying delays to stress the heap scheduler | ||||||
|  |         uint32_t delay = 1 + (callback_id % 50); | ||||||
|  |  | ||||||
|  |         // Also test nested scheduling from callbacks | ||||||
|  |         if (j % 10 == 0) { | ||||||
|  |           // Every 10th callback schedules another callback | ||||||
|  |           this->set_timeout(dynamic_name, delay, [component, callback_id]() { | ||||||
|  |             component->executed_callbacks_.fetch_add(1); | ||||||
|  |             ESP_LOGV(TAG, "Executed string-named callback %d (nested scheduler)", callback_id); | ||||||
|  |  | ||||||
|  |             // Schedule another timeout from within this callback with a new dynamic name | ||||||
|  |             std::string nested_name = "nested_from_" + std::to_string(callback_id); | ||||||
|  |             component->set_timeout(nested_name, 1, [callback_id]() { | ||||||
|  |               ESP_LOGV(TAG, "Executed nested string-named callback from %d", callback_id); | ||||||
|  |             }); | ||||||
|  |           }); | ||||||
|  |         } else { | ||||||
|  |           // Regular callback | ||||||
|  |           this->set_timeout(dynamic_name, delay, [component, callback_id]() { | ||||||
|  |             component->executed_callbacks_.fetch_add(1); | ||||||
|  |             ESP_LOGV(TAG, "Executed string-named callback %d", callback_id); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Add some timing variations to increase race conditions | ||||||
|  |         if (j % 5 == 0) { | ||||||
|  |           std::this_thread::sleep_for(std::chrono::microseconds(100)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       ESP_LOGV(TAG, "Thread %d finished scheduling", i); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Wait for all threads to complete scheduling | ||||||
|  |   for (auto &t : threads) { | ||||||
|  |     t.join(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   auto end_time = std::chrono::steady_clock::now(); | ||||||
|  |   auto thread_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count(); | ||||||
|  |   ESP_LOGI(TAG, "All threads finished scheduling in %lldms. Created %d callbacks with dynamic names", thread_time, | ||||||
|  |            this->total_callbacks_.load()); | ||||||
|  |  | ||||||
|  |   // Give some time for callbacks to execute | ||||||
|  |   ESP_LOGI(TAG, "Waiting for callbacks to execute..."); | ||||||
|  |  | ||||||
|  |   // Schedule a final callback to signal completion | ||||||
|  |   this->set_timeout("test_complete", 2000, [this]() { | ||||||
|  |     ESP_LOGI(TAG, "String name stress test complete. Executed %d of %d callbacks", this->executed_callbacks_.load(), | ||||||
|  |              this->total_callbacks_.load()); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_string_name_stress_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include <atomic> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_string_name_stress_component { | ||||||
|  |  | ||||||
|  | class SchedulerStringNameStressComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_string_name_stress_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   std::atomic<int> total_callbacks_{0}; | ||||||
|  |   std::atomic<int> executed_callbacks_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_string_name_stress_component | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										23
									
								
								tests/integration/fixtures/scheduler_bulk_cleanup.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								tests/integration/fixtures/scheduler_bulk_cleanup.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | esphome: | ||||||
|  |   name: scheduler-bulk-cleanup | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: DEBUG | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: trigger_bulk_cleanup | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             auto component = id(bulk_cleanup_component); | ||||||
|  |             component->trigger_bulk_cleanup(); | ||||||
|  |  | ||||||
|  | scheduler_bulk_cleanup_component: | ||||||
|  |   id: bulk_cleanup_component | ||||||
							
								
								
									
										51
									
								
								tests/integration/fixtures/scheduler_defer_cancel.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								tests/integration/fixtures/scheduler_defer_cancel.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | esphome: | ||||||
|  |   name: scheduler-defer-cancel | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: DEBUG | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: test_defer_cancel | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             // Schedule 10 defers with the same name | ||||||
|  |             // Only the last one should execute | ||||||
|  |             for (int i = 1; i <= 10; i++) { | ||||||
|  |               App.scheduler.set_timeout(nullptr, "test_defer", 0, [i]() { | ||||||
|  |                 ESP_LOGI("TEST", "Defer executed: %d", i); | ||||||
|  |                 // Fire event with the defer number | ||||||
|  |                 std::string event_type = "defer_executed_" + std::to_string(i); | ||||||
|  |                 id(test_result)->trigger(event_type); | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Schedule completion notification after all defers | ||||||
|  |             App.scheduler.set_timeout(nullptr, "completion", 0, []() { | ||||||
|  |               ESP_LOGI("TEST", "Test complete"); | ||||||
|  |               id(test_complete)->trigger("test_finished"); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  | event: | ||||||
|  |   - platform: template | ||||||
|  |     id: test_result | ||||||
|  |     name: "Test Result" | ||||||
|  |     event_types: | ||||||
|  |       - "defer_executed_1" | ||||||
|  |       - "defer_executed_2" | ||||||
|  |       - "defer_executed_3" | ||||||
|  |       - "defer_executed_4" | ||||||
|  |       - "defer_executed_5" | ||||||
|  |       - "defer_executed_6" | ||||||
|  |       - "defer_executed_7" | ||||||
|  |       - "defer_executed_8" | ||||||
|  |       - "defer_executed_9" | ||||||
|  |       - "defer_executed_10" | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     id: test_complete | ||||||
|  |     name: "Test Complete" | ||||||
|  |     event_types: | ||||||
|  |       - "test_finished" | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | esphome: | ||||||
|  |   name: scheduler-defer-cancel-regular | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: DEBUG | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: test_defer_cancels_regular | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             ESP_LOGI("TEST", "Starting defer cancels regular timeout test"); | ||||||
|  |  | ||||||
|  |             // Schedule a regular timeout with 100ms delay | ||||||
|  |             App.scheduler.set_timeout(nullptr, "test_timeout", 100, []() { | ||||||
|  |               ESP_LOGE("TEST", "ERROR: Regular timeout executed - should have been cancelled!"); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             ESP_LOGI("TEST", "Scheduled regular timeout with 100ms delay"); | ||||||
|  |  | ||||||
|  |             // Immediately schedule a deferred timeout (0 delay) with the same name | ||||||
|  |             // This should cancel the regular timeout | ||||||
|  |             App.scheduler.set_timeout(nullptr, "test_timeout", 0, []() { | ||||||
|  |               ESP_LOGI("TEST", "SUCCESS: Deferred timeout executed"); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             ESP_LOGI("TEST", "Scheduled deferred timeout - should cancel regular timeout"); | ||||||
|  |  | ||||||
|  |             // Schedule test completion after 200ms (after regular timeout would have fired) | ||||||
|  |             App.scheduler.set_timeout(nullptr, "test_complete", 200, []() { | ||||||
|  |               ESP_LOGI("TEST", "Test complete"); | ||||||
|  |             }); | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| esphome: | esphome: | ||||||
|   name: defer-fifo-simple |   name: scheduler-defer-fifo-simple | ||||||
| 
 | 
 | ||||||
| host: | host: | ||||||
| 
 | 
 | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| esphome: | esphome: | ||||||
|   name: defer-stress-test |   name: scheduler-defer-stress-test | ||||||
| 
 | 
 | ||||||
| external_components: | external_components: | ||||||
|   - source: |   - source: | ||||||
							
								
								
									
										38
									
								
								tests/integration/fixtures/scheduler_heap_stress.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tests/integration/fixtures/scheduler_heap_stress.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | esphome: | ||||||
|  |   name: scheduler-heap-stress-test | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_heap_stress_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | scheduler_heap_stress_component: | ||||||
|  |   id: heap_stress | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_heap_stress_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(heap_stress)->run_multi_thread_test(); | ||||||
|  |  | ||||||
|  | event: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Complete" | ||||||
|  |     id: test_complete | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "test_finished" | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Result" | ||||||
|  |     id: test_result | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "passed" | ||||||
|  |       - "failed" | ||||||
							
								
								
									
										38
									
								
								tests/integration/fixtures/scheduler_rapid_cancellation.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tests/integration/fixtures/scheduler_rapid_cancellation.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | esphome: | ||||||
|  |   name: sched-rapid-cancel-test | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_rapid_cancellation_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | scheduler_rapid_cancellation_component: | ||||||
|  |   id: rapid_cancel | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_rapid_cancellation_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(rapid_cancel)->run_rapid_cancellation_test(); | ||||||
|  |  | ||||||
|  | event: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Complete" | ||||||
|  |     id: test_complete | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "test_finished" | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Result" | ||||||
|  |     id: test_result | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "passed" | ||||||
|  |       - "failed" | ||||||
							
								
								
									
										38
									
								
								tests/integration/fixtures/scheduler_recursive_timeout.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tests/integration/fixtures/scheduler_recursive_timeout.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | esphome: | ||||||
|  |   name: sched-recursive-timeout | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_recursive_timeout_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | scheduler_recursive_timeout_component: | ||||||
|  |   id: recursive_timeout | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_recursive_timeout_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(recursive_timeout)->run_recursive_timeout_test(); | ||||||
|  |  | ||||||
|  | event: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Complete" | ||||||
|  |     id: test_complete | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "test_finished" | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Result" | ||||||
|  |     id: test_result | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "passed" | ||||||
|  |       - "failed" | ||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | esphome: | ||||||
|  |   name: sched-simul-callbacks-test | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_simultaneous_callbacks_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: INFO | ||||||
|  |  | ||||||
|  | scheduler_simultaneous_callbacks_component: | ||||||
|  |   id: simultaneous_callbacks | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_simultaneous_callbacks_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(simultaneous_callbacks)->run_simultaneous_callbacks_test(); | ||||||
							
								
								
									
										47
									
								
								tests/integration/fixtures/scheduler_string_lifetime.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								tests/integration/fixtures/scheduler_string_lifetime.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | esphome: | ||||||
|  |   name: scheduler-string-lifetime-test | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_string_lifetime_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: DEBUG | ||||||
|  |  | ||||||
|  | scheduler_string_lifetime_component: | ||||||
|  |   id: string_lifetime | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_string_lifetime_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(string_lifetime)->run_string_lifetime_test(); | ||||||
|  |     - service: run_test1 | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(string_lifetime)->run_test1(); | ||||||
|  |     - service: run_test2 | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(string_lifetime)->run_test2(); | ||||||
|  |     - service: run_test3 | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(string_lifetime)->run_test3(); | ||||||
|  |     - service: run_test4 | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(string_lifetime)->run_test4(); | ||||||
|  |     - service: run_test5 | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(string_lifetime)->run_test5(); | ||||||
|  |     - service: run_final_check | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(string_lifetime)->run_final_check(); | ||||||
							
								
								
									
										38
									
								
								tests/integration/fixtures/scheduler_string_name_stress.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tests/integration/fixtures/scheduler_string_name_stress.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | esphome: | ||||||
|  |   name: sched-string-name-stress | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_string_name_stress_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | scheduler_string_name_stress_component: | ||||||
|  |   id: string_stress | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_string_name_stress_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(string_stress)->run_string_name_stress_test(); | ||||||
|  |  | ||||||
|  | event: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Complete" | ||||||
|  |     id: test_complete | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "test_finished" | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Result" | ||||||
|  |     id: test_result | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "passed" | ||||||
|  |       - "failed" | ||||||
							
								
								
									
										122
									
								
								tests/integration/test_scheduler_bulk_cleanup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								tests/integration/test_scheduler_bulk_cleanup.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | """Test that triggers the bulk cleanup path when to_remove_ > MAX_LOGICALLY_DELETED_ITEMS.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_bulk_cleanup( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that bulk cleanup path is triggered when many items are cancelled.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_event_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |     bulk_cleanup_triggered = False | ||||||
|  |     cleanup_stats: dict[str, int] = { | ||||||
|  |         "removed": 0, | ||||||
|  |         "before": 0, | ||||||
|  |         "after": 0, | ||||||
|  |     } | ||||||
|  |     post_cleanup_executed = 0 | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         nonlocal bulk_cleanup_triggered, post_cleanup_executed | ||||||
|  |  | ||||||
|  |         # Look for logs indicating bulk cleanup was triggered | ||||||
|  |         # The actual cleanup happens silently, so we track the cancel operations | ||||||
|  |         if "Successfully cancelled" in line and "timeouts" in line: | ||||||
|  |             match = re.search(r"Successfully cancelled (\d+) timeouts", line) | ||||||
|  |             if match and int(match.group(1)) > 10: | ||||||
|  |                 bulk_cleanup_triggered = True | ||||||
|  |  | ||||||
|  |         # Track cleanup statistics | ||||||
|  |         match = re.search(r"Bulk cleanup triggered: removed (\d+) items", line) | ||||||
|  |         if match: | ||||||
|  |             cleanup_stats["removed"] = int(match.group(1)) | ||||||
|  |  | ||||||
|  |         match = re.search(r"Items before cleanup: (\d+), after: (\d+)", line) | ||||||
|  |         if match: | ||||||
|  |             cleanup_stats["before"] = int(match.group(1)) | ||||||
|  |             cleanup_stats["after"] = int(match.group(2)) | ||||||
|  |  | ||||||
|  |         # Track post-cleanup timeout executions | ||||||
|  |         if "Post-cleanup timeout" in line and "executed correctly" in line: | ||||||
|  |             match = re.search(r"Post-cleanup timeout (\d+) executed correctly", line) | ||||||
|  |             if match: | ||||||
|  |                 post_cleanup_executed += 1 | ||||||
|  |  | ||||||
|  |         # Check for final test completion | ||||||
|  |         if ( | ||||||
|  |             "All post-cleanup timeouts completed - test finished" in line | ||||||
|  |             and not test_complete_future.done() | ||||||
|  |         ): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "scheduler-bulk-cleanup" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         trigger_bulk_cleanup_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "trigger_bulk_cleanup": | ||||||
|  |                 trigger_bulk_cleanup_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert trigger_bulk_cleanup_service is not None, ( | ||||||
|  |             "trigger_bulk_cleanup service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Execute the test | ||||||
|  |         client.execute_service(trigger_bulk_cleanup_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test completion | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=10.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Bulk cleanup test timed out") | ||||||
|  |  | ||||||
|  |         # Verify bulk cleanup was triggered | ||||||
|  |         assert bulk_cleanup_triggered, ( | ||||||
|  |             "Bulk cleanup path was not triggered - MAX_LOGICALLY_DELETED_ITEMS threshold not reached" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify cleanup statistics | ||||||
|  |         assert cleanup_stats["removed"] > 10, ( | ||||||
|  |             f"Expected more than 10 items removed, got {cleanup_stats['removed']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify scheduler still works after bulk cleanup | ||||||
|  |         assert post_cleanup_executed == 5, ( | ||||||
|  |             f"Expected 5 post-cleanup timeouts to execute, but {post_cleanup_executed} executed" | ||||||
|  |         ) | ||||||
							
								
								
									
										94
									
								
								tests/integration/test_scheduler_defer_cancel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								tests/integration/test_scheduler_defer_cancel.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | |||||||
|  | """Test that defer() with the same name cancels previous defers.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
|  | from aioesphomeapi import EntityState, Event, EventInfo, UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_defer_cancel( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that defer() with the same name cancels previous defers.""" | ||||||
|  |  | ||||||
|  |     async with run_compiled(yaml_config), api_client_connected() as client: | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "scheduler-defer-cancel" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         entity_info, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test entities | ||||||
|  |         test_complete_entity: EventInfo | None = None | ||||||
|  |         test_result_entity: EventInfo | None = None | ||||||
|  |  | ||||||
|  |         for entity in entity_info: | ||||||
|  |             if isinstance(entity, EventInfo): | ||||||
|  |                 if entity.object_id == "test_complete": | ||||||
|  |                     test_complete_entity = entity | ||||||
|  |                 elif entity.object_id == "test_result": | ||||||
|  |                     test_result_entity = entity | ||||||
|  |  | ||||||
|  |         assert test_complete_entity is not None, "test_complete event not found" | ||||||
|  |         assert test_result_entity is not None, "test_result event not found" | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         test_defer_cancel_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "test_defer_cancel": | ||||||
|  |                 test_defer_cancel_service = service | ||||||
|  |  | ||||||
|  |         assert test_defer_cancel_service is not None, ( | ||||||
|  |             "test_defer_cancel service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Get the event loop | ||||||
|  |         loop = asyncio.get_running_loop() | ||||||
|  |  | ||||||
|  |         # Subscribe to states | ||||||
|  |         test_complete_future: asyncio.Future[bool] = loop.create_future() | ||||||
|  |         test_result_future: asyncio.Future[int] = loop.create_future() | ||||||
|  |  | ||||||
|  |         def on_state(state: EntityState) -> None: | ||||||
|  |             if not isinstance(state, Event): | ||||||
|  |                 return | ||||||
|  |  | ||||||
|  |             if ( | ||||||
|  |                 state.key == test_complete_entity.key | ||||||
|  |                 and state.event_type == "test_finished" | ||||||
|  |                 and not test_complete_future.done() | ||||||
|  |             ): | ||||||
|  |                 test_complete_future.set_result(True) | ||||||
|  |                 return | ||||||
|  |  | ||||||
|  |             if state.key == test_result_entity.key and not test_result_future.done(): | ||||||
|  |                 # Event type should be "defer_executed_X" where X is the defer number | ||||||
|  |                 if state.event_type.startswith("defer_executed_"): | ||||||
|  |                     defer_num = int(state.event_type.split("_")[-1]) | ||||||
|  |                     test_result_future.set_result(defer_num) | ||||||
|  |  | ||||||
|  |         client.subscribe_states(on_state) | ||||||
|  |  | ||||||
|  |         # Execute the test | ||||||
|  |         client.execute_service(test_defer_cancel_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test completion | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=10.0) | ||||||
|  |             executed_defer = await asyncio.wait_for(test_result_future, timeout=1.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Test did not complete within timeout") | ||||||
|  |  | ||||||
|  |         # Verify that only defer 10 was executed | ||||||
|  |         assert executed_defer == 10, ( | ||||||
|  |             f"Expected defer 10 to execute, got {executed_defer}" | ||||||
|  |         ) | ||||||
							
								
								
									
										90
									
								
								tests/integration/test_scheduler_defer_cancel_regular.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								tests/integration/test_scheduler_defer_cancel_regular.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | """Test that a deferred timeout cancels a regular timeout with the same name.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_defer_cancels_regular( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that set_timeout(name, 0) cancels a previously scheduled set_timeout(name, delay).""" | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track log messages | ||||||
|  |     log_messages: list[str] = [] | ||||||
|  |     error_detected = False | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         nonlocal error_detected | ||||||
|  |         if "TEST" in line: | ||||||
|  |             log_messages.append(line) | ||||||
|  |  | ||||||
|  |         if "ERROR: Regular timeout executed" in line: | ||||||
|  |             error_detected = True | ||||||
|  |  | ||||||
|  |         if "Test complete" in line and not test_complete_future.done(): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "scheduler-defer-cancel-regular" | ||||||
|  |  | ||||||
|  |         # List services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "test_defer_cancels_regular": | ||||||
|  |                 test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert test_service is not None, "test_defer_cancels_regular service not found" | ||||||
|  |  | ||||||
|  |         # Execute the test | ||||||
|  |         client.execute_service(test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test completion | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=5.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail(f"Test timed out. Log messages: {log_messages}") | ||||||
|  |  | ||||||
|  |         # Verify results | ||||||
|  |         assert not error_detected, ( | ||||||
|  |             f"Regular timeout should have been cancelled but it executed! Logs: {log_messages}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify the deferred timeout executed | ||||||
|  |         assert any( | ||||||
|  |             "SUCCESS: Deferred timeout executed" in msg for msg in log_messages | ||||||
|  |         ), f"Deferred timeout should have executed. Logs: {log_messages}" | ||||||
|  |  | ||||||
|  |         # Verify the expected sequence of events | ||||||
|  |         assert any( | ||||||
|  |             "Starting defer cancels regular timeout test" in msg for msg in log_messages | ||||||
|  |         ) | ||||||
|  |         assert any( | ||||||
|  |             "Scheduled regular timeout with 100ms delay" in msg for msg in log_messages | ||||||
|  |         ) | ||||||
|  |         assert any( | ||||||
|  |             "Scheduled deferred timeout - should cancel regular timeout" in msg | ||||||
|  |             for msg in log_messages | ||||||
|  |         ) | ||||||
| @@ -9,7 +9,7 @@ from .types import APIClientConnectedFactory, RunCompiledFunction | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.asyncio | @pytest.mark.asyncio | ||||||
| async def test_defer_fifo_simple( | async def test_scheduler_defer_fifo_simple( | ||||||
|     yaml_config: str, |     yaml_config: str, | ||||||
|     run_compiled: RunCompiledFunction, |     run_compiled: RunCompiledFunction, | ||||||
|     api_client_connected: APIClientConnectedFactory, |     api_client_connected: APIClientConnectedFactory, | ||||||
| @@ -20,7 +20,7 @@ async def test_defer_fifo_simple( | |||||||
|         # Verify we can connect |         # Verify we can connect | ||||||
|         device_info = await client.device_info() |         device_info = await client.device_info() | ||||||
|         assert device_info is not None |         assert device_info is not None | ||||||
|         assert device_info.name == "defer-fifo-simple" |         assert device_info.name == "scheduler-defer-fifo-simple" | ||||||
| 
 | 
 | ||||||
|         # List entities and services |         # List entities and services | ||||||
|         entity_info, services = await asyncio.wait_for( |         entity_info, services = await asyncio.wait_for( | ||||||
| @@ -11,7 +11,7 @@ from .types import APIClientConnectedFactory, RunCompiledFunction | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.asyncio | @pytest.mark.asyncio | ||||||
| async def test_defer_stress( | async def test_scheduler_defer_stress( | ||||||
|     yaml_config: str, |     yaml_config: str, | ||||||
|     run_compiled: RunCompiledFunction, |     run_compiled: RunCompiledFunction, | ||||||
|     api_client_connected: APIClientConnectedFactory, |     api_client_connected: APIClientConnectedFactory, | ||||||
| @@ -75,7 +75,7 @@ async def test_defer_stress( | |||||||
|         # Verify we can connect |         # Verify we can connect | ||||||
|         device_info = await client.device_info() |         device_info = await client.device_info() | ||||||
|         assert device_info is not None |         assert device_info is not None | ||||||
|         assert device_info.name == "defer-stress-test" |         assert device_info.name == "scheduler-defer-stress-test" | ||||||
| 
 | 
 | ||||||
|         # List entities and services |         # List entities and services | ||||||
|         entity_info, services = await asyncio.wait_for( |         entity_info, services = await asyncio.wait_for( | ||||||
							
								
								
									
										140
									
								
								tests/integration/test_scheduler_heap_stress.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								tests/integration/test_scheduler_heap_stress.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | """Stress test for heap scheduler thread safety with multiple threads.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_heap_stress( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that set_timeout/set_interval doesn't crash when called rapidly from multiple threads.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track executed timeouts/intervals and their order | ||||||
|  |     executed_callbacks: set[int] = set() | ||||||
|  |     thread_executions: dict[ | ||||||
|  |         int, list[int] | ||||||
|  |     ] = {}  # thread_id -> list of indices in execution order | ||||||
|  |     callback_types: dict[int, str] = {}  # callback_id -> "timeout" or "interval" | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Track all executed callbacks with thread and index info | ||||||
|  |         match = re.search( | ||||||
|  |             r"Executed (timeout|interval) (\d+) \(thread (\d+), index (\d+)\)", line | ||||||
|  |         ) | ||||||
|  |         if not match: | ||||||
|  |             # Also check for the completion message | ||||||
|  |             if "All threads finished" in line and "Created 1000 callbacks" in line: | ||||||
|  |                 # Give scheduler some time to execute callbacks | ||||||
|  |                 pass | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         callback_type = match.group(1) | ||||||
|  |         callback_id = int(match.group(2)) | ||||||
|  |         thread_id = int(match.group(3)) | ||||||
|  |         index = int(match.group(4)) | ||||||
|  |  | ||||||
|  |         # Only count each callback ID once (intervals might fire multiple times) | ||||||
|  |         if callback_id not in executed_callbacks: | ||||||
|  |             executed_callbacks.add(callback_id) | ||||||
|  |             callback_types[callback_id] = callback_type | ||||||
|  |  | ||||||
|  |         # Track execution order per thread | ||||||
|  |         if thread_id not in thread_executions: | ||||||
|  |             thread_executions[thread_id] = [] | ||||||
|  |  | ||||||
|  |         # Only append if this is a new execution for this thread | ||||||
|  |         if index not in thread_executions[thread_id]: | ||||||
|  |             thread_executions[thread_id].append(index) | ||||||
|  |  | ||||||
|  |         # Check if we've executed all 1000 callbacks (0-999) | ||||||
|  |         if len(executed_callbacks) >= 1000 and not test_complete_future.done(): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "scheduler-heap-stress-test" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_stress_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_heap_stress_test": | ||||||
|  |                 run_stress_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_stress_test_service is not None, ( | ||||||
|  |             "run_heap_stress_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the run_heap_stress_test service to start the test | ||||||
|  |         client.execute_service(run_stress_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for all callbacks to execute (should be quick, but give more time for scheduling) | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=60.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             # Report how many we got | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Stress test timed out. Only {len(executed_callbacks)} of " | ||||||
|  |                 f"1000 callbacks executed. Missing IDs: " | ||||||
|  |                 f"{sorted(set(range(1000)) - executed_callbacks)[:10]}..." | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Verify all callbacks executed | ||||||
|  |         assert len(executed_callbacks) == 1000, ( | ||||||
|  |             f"Expected 1000 callbacks, got {len(executed_callbacks)}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify we have all IDs from 0-999 | ||||||
|  |         expected_ids = set(range(1000)) | ||||||
|  |         missing_ids = expected_ids - executed_callbacks | ||||||
|  |         assert not missing_ids, f"Missing callback IDs: {sorted(missing_ids)}" | ||||||
|  |  | ||||||
|  |         # Verify we have a mix of timeouts and intervals | ||||||
|  |         timeout_count = sum(1 for t in callback_types.values() if t == "timeout") | ||||||
|  |         interval_count = sum(1 for t in callback_types.values() if t == "interval") | ||||||
|  |         assert timeout_count > 0, "No timeouts were executed" | ||||||
|  |         assert interval_count > 0, "No intervals were executed" | ||||||
|  |  | ||||||
|  |         # Verify each thread executed callbacks | ||||||
|  |         for thread_id, indices in thread_executions.items(): | ||||||
|  |             assert len(indices) == 100, ( | ||||||
|  |                 f"Thread {thread_id} executed {len(indices)} callbacks, expected 100" | ||||||
|  |             ) | ||||||
|  |         # Total should be 1000 callbacks | ||||||
|  |         total_callbacks = timeout_count + interval_count | ||||||
|  |         assert total_callbacks == 1000, ( | ||||||
|  |             f"Expected 1000 total callbacks but got {total_callbacks}" | ||||||
|  |         ) | ||||||
							
								
								
									
										142
									
								
								tests/integration/test_scheduler_rapid_cancellation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								tests/integration/test_scheduler_rapid_cancellation.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | |||||||
|  | """Rapid cancellation test - schedule and immediately cancel timeouts with string names.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_rapid_cancellation( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test rapid schedule/cancel cycles that might expose race conditions.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track test progress | ||||||
|  |     test_stats = { | ||||||
|  |         "log_count": 0, | ||||||
|  |         "errors": [], | ||||||
|  |         "summary_scheduled": None, | ||||||
|  |         "final_scheduled": 0, | ||||||
|  |         "final_executed": 0, | ||||||
|  |         "final_implicit_cancellations": 0, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Count log lines | ||||||
|  |         test_stats["log_count"] += 1 | ||||||
|  |  | ||||||
|  |         # Check for errors (only ERROR level, not WARN) | ||||||
|  |         if "ERROR" in line: | ||||||
|  |             test_stats["errors"].append(line) | ||||||
|  |  | ||||||
|  |         # Parse summary statistics | ||||||
|  |         if "All threads completed. Scheduled:" in line: | ||||||
|  |             # Extract the scheduled count from the summary | ||||||
|  |             if match := re.search(r"Scheduled: (\d+)", line): | ||||||
|  |                 test_stats["summary_scheduled"] = int(match.group(1)) | ||||||
|  |         elif "Total scheduled:" in line: | ||||||
|  |             if match := re.search(r"Total scheduled: (\d+)", line): | ||||||
|  |                 test_stats["final_scheduled"] = int(match.group(1)) | ||||||
|  |         elif "Total executed:" in line: | ||||||
|  |             if match := re.search(r"Total executed: (\d+)", line): | ||||||
|  |                 test_stats["final_executed"] = int(match.group(1)) | ||||||
|  |         elif "Implicit cancellations (replaced):" in line: | ||||||
|  |             if match := re.search(r"Implicit cancellations \(replaced\): (\d+)", line): | ||||||
|  |                 test_stats["final_implicit_cancellations"] = int(match.group(1)) | ||||||
|  |  | ||||||
|  |         # Check for crash indicators | ||||||
|  |         if any( | ||||||
|  |             indicator in line.lower() | ||||||
|  |             for indicator in ["segfault", "abort", "assertion", "heap corruption"] | ||||||
|  |         ): | ||||||
|  |             if not test_complete_future.done(): | ||||||
|  |                 test_complete_future.set_exception(Exception(f"Crash detected: {line}")) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         # Check for completion - wait for final message after all stats are logged | ||||||
|  |         if ( | ||||||
|  |             "Test finished - all statistics reported" in line | ||||||
|  |             and not test_complete_future.done() | ||||||
|  |         ): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "sched-rapid-cancel-test" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_rapid_cancellation_test": | ||||||
|  |                 run_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_test_service is not None, ( | ||||||
|  |             "run_rapid_cancellation_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the service to start the test | ||||||
|  |         client.execute_service(run_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test to complete with timeout | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=10.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail(f"Test timed out. Stats: {test_stats}") | ||||||
|  |  | ||||||
|  |         # Check for any errors | ||||||
|  |         assert len(test_stats["errors"]) == 0, ( | ||||||
|  |             f"Errors detected: {test_stats['errors']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Check that we received log messages | ||||||
|  |         assert test_stats["log_count"] > 0, "No log messages received" | ||||||
|  |  | ||||||
|  |         # Check the summary line to verify all threads scheduled their operations | ||||||
|  |         assert test_stats["summary_scheduled"] == 400, ( | ||||||
|  |             f"Expected summary to show 400 scheduled operations but got {test_stats['summary_scheduled']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Check final statistics | ||||||
|  |         assert test_stats["final_scheduled"] == 400, ( | ||||||
|  |             f"Expected final stats to show 400 scheduled but got {test_stats['final_scheduled']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         assert test_stats["final_executed"] == 10, ( | ||||||
|  |             f"Expected final stats to show 10 executed but got {test_stats['final_executed']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         assert test_stats["final_implicit_cancellations"] == 390, ( | ||||||
|  |             f"Expected final stats to show 390 implicit cancellations but got {test_stats['final_implicit_cancellations']}" | ||||||
|  |         ) | ||||||
							
								
								
									
										101
									
								
								tests/integration/test_scheduler_recursive_timeout.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								tests/integration/test_scheduler_recursive_timeout.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | """Test for recursive timeout scheduling - scheduling timeouts from within timeout callbacks.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_recursive_timeout( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that scheduling timeouts from within timeout callbacks works correctly.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track execution sequence | ||||||
|  |     execution_sequence: list[str] = [] | ||||||
|  |     expected_sequence = [ | ||||||
|  |         "initial_timeout", | ||||||
|  |         "nested_timeout_1", | ||||||
|  |         "nested_timeout_2", | ||||||
|  |         "test_complete", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Track execution sequence | ||||||
|  |         if "Executing initial timeout" in line: | ||||||
|  |             execution_sequence.append("initial_timeout") | ||||||
|  |         elif "Executing nested timeout 1" in line: | ||||||
|  |             execution_sequence.append("nested_timeout_1") | ||||||
|  |         elif "Executing nested timeout 2" in line: | ||||||
|  |             execution_sequence.append("nested_timeout_2") | ||||||
|  |         elif "Recursive timeout test complete" in line: | ||||||
|  |             execution_sequence.append("test_complete") | ||||||
|  |             if not test_complete_future.done(): | ||||||
|  |                 test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "sched-recursive-timeout" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_recursive_timeout_test": | ||||||
|  |                 run_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_test_service is not None, ( | ||||||
|  |             "run_recursive_timeout_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the service to start the test | ||||||
|  |         client.execute_service(run_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test to complete | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=10.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Recursive timeout test timed out. Got sequence: {execution_sequence}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Verify execution sequence | ||||||
|  |         assert execution_sequence == expected_sequence, ( | ||||||
|  |             f"Execution sequence mismatch. Expected {expected_sequence}, " | ||||||
|  |             f"got {execution_sequence}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify we got exactly 4 events (Initial + Level 1 + Level 2 + Complete) | ||||||
|  |         assert len(execution_sequence) == 4, ( | ||||||
|  |             f"Expected 4 events but got {len(execution_sequence)}" | ||||||
|  |         ) | ||||||
							
								
								
									
										123
									
								
								tests/integration/test_scheduler_simultaneous_callbacks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								tests/integration/test_scheduler_simultaneous_callbacks.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | |||||||
|  | """Simultaneous callbacks test - schedule many callbacks for the same time from multiple threads.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_simultaneous_callbacks( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test scheduling many callbacks for the exact same time from multiple threads.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track test progress | ||||||
|  |     test_stats = { | ||||||
|  |         "scheduled": 0, | ||||||
|  |         "executed": 0, | ||||||
|  |         "expected": 1000,  # 10 threads * 100 callbacks | ||||||
|  |         "errors": [], | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Track operations | ||||||
|  |         if "Scheduled callback" in line: | ||||||
|  |             test_stats["scheduled"] += 1 | ||||||
|  |         elif "Callback executed" in line: | ||||||
|  |             test_stats["executed"] += 1 | ||||||
|  |         elif "ERROR" in line: | ||||||
|  |             test_stats["errors"].append(line) | ||||||
|  |  | ||||||
|  |         # Check for crash indicators | ||||||
|  |         if any( | ||||||
|  |             indicator in line.lower() | ||||||
|  |             for indicator in ["segfault", "abort", "assertion", "heap corruption"] | ||||||
|  |         ): | ||||||
|  |             if not test_complete_future.done(): | ||||||
|  |                 test_complete_future.set_exception(Exception(f"Crash detected: {line}")) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         # Check for completion with final count | ||||||
|  |         if "Final executed count:" in line: | ||||||
|  |             # Extract number from log line like: "[07:59:47][I][simultaneous_callbacks:093]: Simultaneous callbacks test complete. Final executed count: 1000" | ||||||
|  |             match = re.search(r"Final executed count:\s*(\d+)", line) | ||||||
|  |             if match: | ||||||
|  |                 test_stats["final_count"] = int(match.group(1)) | ||||||
|  |  | ||||||
|  |         # Check for completion | ||||||
|  |         if ( | ||||||
|  |             "Simultaneous callbacks test complete" in line | ||||||
|  |             and not test_complete_future.done() | ||||||
|  |         ): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "sched-simul-callbacks-test" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_simultaneous_callbacks_test": | ||||||
|  |                 run_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_test_service is not None, ( | ||||||
|  |             "run_simultaneous_callbacks_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the service to start the test | ||||||
|  |         client.execute_service(run_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test to complete | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=30.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail(f"Simultaneous callbacks test timed out. Stats: {test_stats}") | ||||||
|  |  | ||||||
|  |         # Check for any errors | ||||||
|  |         assert len(test_stats["errors"]) == 0, ( | ||||||
|  |             f"Errors detected: {test_stats['errors']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify all callbacks executed using the final count from C++ | ||||||
|  |         final_count = test_stats.get("final_count", 0) | ||||||
|  |         assert final_count == test_stats["expected"], ( | ||||||
|  |             f"Expected {test_stats['expected']} callbacks, but only {final_count} executed" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # The final_count is the authoritative count from the C++ component | ||||||
|  |         assert final_count == 1000, ( | ||||||
|  |             f"Expected 1000 executed callbacks but got {final_count}" | ||||||
|  |         ) | ||||||
							
								
								
									
										169
									
								
								tests/integration/test_scheduler_string_lifetime.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								tests/integration/test_scheduler_string_lifetime.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | |||||||
|  | """String lifetime test - verify scheduler handles string destruction correctly.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_string_lifetime( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that scheduler correctly handles string lifetimes when strings go out of scope.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create events for synchronization | ||||||
|  |     test1_complete = asyncio.Event() | ||||||
|  |     test2_complete = asyncio.Event() | ||||||
|  |     test3_complete = asyncio.Event() | ||||||
|  |     test4_complete = asyncio.Event() | ||||||
|  |     test5_complete = asyncio.Event() | ||||||
|  |     all_tests_complete = asyncio.Event() | ||||||
|  |  | ||||||
|  |     # Track test progress | ||||||
|  |     test_stats = { | ||||||
|  |         "tests_passed": 0, | ||||||
|  |         "tests_failed": 0, | ||||||
|  |         "errors": [], | ||||||
|  |         "current_test": None, | ||||||
|  |         "test_callbacks_executed": {}, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Track test-specific events | ||||||
|  |         if "Test 1 complete" in line: | ||||||
|  |             test1_complete.set() | ||||||
|  |         elif "Test 2 complete" in line: | ||||||
|  |             test2_complete.set() | ||||||
|  |         elif "Test 3 complete" in line: | ||||||
|  |             test3_complete.set() | ||||||
|  |         elif "Test 4 complete" in line: | ||||||
|  |             test4_complete.set() | ||||||
|  |         elif "Test 5 complete" in line: | ||||||
|  |             test5_complete.set() | ||||||
|  |  | ||||||
|  |         # Track individual callback executions | ||||||
|  |         callback_match = re.search(r"Callback '(.+?)' executed", line) | ||||||
|  |         if callback_match: | ||||||
|  |             callback_name = callback_match.group(1) | ||||||
|  |             test_stats["test_callbacks_executed"][callback_name] = True | ||||||
|  |  | ||||||
|  |         # Track test results from the C++ test output | ||||||
|  |         if "Tests passed:" in line and "string_lifetime" in line: | ||||||
|  |             # Extract the number from "Tests passed: 32" | ||||||
|  |             match = re.search(r"Tests passed:\s*(\d+)", line) | ||||||
|  |             if match: | ||||||
|  |                 test_stats["tests_passed"] = int(match.group(1)) | ||||||
|  |         elif "Tests failed:" in line and "string_lifetime" in line: | ||||||
|  |             match = re.search(r"Tests failed:\s*(\d+)", line) | ||||||
|  |             if match: | ||||||
|  |                 test_stats["tests_failed"] = int(match.group(1)) | ||||||
|  |         elif "ERROR" in line and "string_lifetime" in line: | ||||||
|  |             test_stats["errors"].append(line) | ||||||
|  |  | ||||||
|  |         # Check for memory corruption indicators | ||||||
|  |         if any( | ||||||
|  |             indicator in line.lower() | ||||||
|  |             for indicator in [ | ||||||
|  |                 "use after free", | ||||||
|  |                 "heap corruption", | ||||||
|  |                 "segfault", | ||||||
|  |                 "abort", | ||||||
|  |                 "assertion", | ||||||
|  |                 "sanitizer", | ||||||
|  |                 "bad memory", | ||||||
|  |                 "invalid pointer", | ||||||
|  |             ] | ||||||
|  |         ): | ||||||
|  |             pytest.fail(f"Memory corruption detected: {line}") | ||||||
|  |  | ||||||
|  |         # Check for completion | ||||||
|  |         if "String lifetime tests complete" in line: | ||||||
|  |             all_tests_complete.set() | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "scheduler-string-lifetime-test" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test services | ||||||
|  |         test_services = {} | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_test1": | ||||||
|  |                 test_services["test1"] = service | ||||||
|  |             elif service.name == "run_test2": | ||||||
|  |                 test_services["test2"] = service | ||||||
|  |             elif service.name == "run_test3": | ||||||
|  |                 test_services["test3"] = service | ||||||
|  |             elif service.name == "run_test4": | ||||||
|  |                 test_services["test4"] = service | ||||||
|  |             elif service.name == "run_test5": | ||||||
|  |                 test_services["test5"] = service | ||||||
|  |             elif service.name == "run_final_check": | ||||||
|  |                 test_services["final"] = service | ||||||
|  |  | ||||||
|  |         # Ensure all services are found | ||||||
|  |         required_services = ["test1", "test2", "test3", "test4", "test5", "final"] | ||||||
|  |         for service_name in required_services: | ||||||
|  |             assert service_name in test_services, f"{service_name} service not found" | ||||||
|  |  | ||||||
|  |         # Run tests sequentially, waiting for each to complete | ||||||
|  |         try: | ||||||
|  |             # Test 1 | ||||||
|  |             client.execute_service(test_services["test1"], {}) | ||||||
|  |             await asyncio.wait_for(test1_complete.wait(), timeout=5.0) | ||||||
|  |  | ||||||
|  |             # Test 2 | ||||||
|  |             client.execute_service(test_services["test2"], {}) | ||||||
|  |             await asyncio.wait_for(test2_complete.wait(), timeout=5.0) | ||||||
|  |  | ||||||
|  |             # Test 3 | ||||||
|  |             client.execute_service(test_services["test3"], {}) | ||||||
|  |             await asyncio.wait_for(test3_complete.wait(), timeout=5.0) | ||||||
|  |  | ||||||
|  |             # Test 4 | ||||||
|  |             client.execute_service(test_services["test4"], {}) | ||||||
|  |             await asyncio.wait_for(test4_complete.wait(), timeout=5.0) | ||||||
|  |  | ||||||
|  |             # Test 5 | ||||||
|  |             client.execute_service(test_services["test5"], {}) | ||||||
|  |             await asyncio.wait_for(test5_complete.wait(), timeout=5.0) | ||||||
|  |  | ||||||
|  |             # Final check | ||||||
|  |             client.execute_service(test_services["final"], {}) | ||||||
|  |             await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0) | ||||||
|  |  | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail(f"String lifetime test timed out. Stats: {test_stats}") | ||||||
|  |  | ||||||
|  |         # Check for any errors | ||||||
|  |         assert test_stats["tests_failed"] == 0, f"Tests failed: {test_stats['errors']}" | ||||||
|  |  | ||||||
|  |         # Verify we had the expected number of passing tests | ||||||
|  |         assert test_stats["tests_passed"] == 30, ( | ||||||
|  |             f"Expected exactly 30 tests to pass, but got {test_stats['tests_passed']}" | ||||||
|  |         ) | ||||||
							
								
								
									
										116
									
								
								tests/integration/test_scheduler_string_name_stress.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								tests/integration/test_scheduler_string_name_stress.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | |||||||
|  | """Stress test for heap scheduler with std::string names from multiple threads.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_string_name_stress( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that set_timeout/set_interval with std::string names doesn't crash when called from multiple threads.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track executed callbacks and any crashes | ||||||
|  |     executed_callbacks: set[int] = set() | ||||||
|  |     error_messages: list[str] = [] | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Check for crash indicators | ||||||
|  |         if any( | ||||||
|  |             indicator in line.lower() | ||||||
|  |             for indicator in [ | ||||||
|  |                 "segfault", | ||||||
|  |                 "abort", | ||||||
|  |                 "assertion", | ||||||
|  |                 "heap corruption", | ||||||
|  |                 "use after free", | ||||||
|  |             ] | ||||||
|  |         ): | ||||||
|  |             error_messages.append(line) | ||||||
|  |             if not test_complete_future.done(): | ||||||
|  |                 test_complete_future.set_exception(Exception(f"Crash detected: {line}")) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         # Track executed callbacks | ||||||
|  |         match = re.search(r"Executed string-named callback (\d+)", line) | ||||||
|  |         if match: | ||||||
|  |             callback_id = int(match.group(1)) | ||||||
|  |             executed_callbacks.add(callback_id) | ||||||
|  |  | ||||||
|  |         # Check for completion | ||||||
|  |         if ( | ||||||
|  |             "String name stress test complete" in line | ||||||
|  |             and not test_complete_future.done() | ||||||
|  |         ): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "sched-string-name-stress" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_stress_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_string_name_stress_test": | ||||||
|  |                 run_stress_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_stress_test_service is not None, ( | ||||||
|  |             "run_string_name_stress_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the service to start the test | ||||||
|  |         client.execute_service(run_stress_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test to complete or crash | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=30.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"String name stress test timed out. Executed {len(executed_callbacks)} callbacks. " | ||||||
|  |                 f"This might indicate a deadlock." | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Verify no errors occurred (crashes already handled by exception) | ||||||
|  |         assert not error_messages, f"Errors detected during test: {error_messages}" | ||||||
|  |  | ||||||
|  |         # Verify we executed all 1000 callbacks (10 threads × 100 callbacks each) | ||||||
|  |         assert len(executed_callbacks) == 1000, ( | ||||||
|  |             f"Expected 1000 callbacks but got {len(executed_callbacks)}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify each callback ID was executed exactly once | ||||||
|  |         for i in range(1000): | ||||||
|  |             assert i in executed_callbacks, f"Callback {i} was not executed" | ||||||
		Reference in New Issue
	
	Block a user