diff --git a/CODEOWNERS b/CODEOWNERS index 667a44fc03e..fee0e98f46d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -155,6 +155,7 @@ esphome/components/esp32_ble_tracker/* @bdraco esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron esphome/components/esp32_hosted/* @swoboda1337 +esphome/components/esp32_hosted/update/* @swoboda1337 esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 231b39a7111..62fa391e5fa 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -8,8 +8,7 @@ #define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] -namespace esphome { -namespace debug { +namespace esphome::debug { static const char *const TAG = "debug"; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; @@ -281,14 +280,18 @@ void DebugComponent::get_device_info_(std::string &device_info) { NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE)); ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); + bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] && + (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected + << UICR_PSELRESET_CONNECT_Pos; ESP_LOGD( TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), - YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) != - (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) || - ((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) != - (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)))); - + YESNO(n_reset_enabled)); + if (n_reset_enabled) { + uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; + uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; + ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin); + } #ifdef USE_BOOTLOADER_MCUBOOT ESP_LOGD(TAG, "bootloader: mcuboot"); #else @@ -322,10 +325,22 @@ void DebugComponent::get_device_info_(std::string &device_info) { #endif } #endif + auto uicr = [](volatile uint32_t *data, uint8_t size) { + std::string res; + char buf[sizeof(uint32_t) * 2 + 1]; + for (size_t i = 0; i < size; i++) { + if (i > 0) { + res += ' '; + } + res += format_hex_pretty(data[i], '\0', false); + } + return res; + }; + ESP_LOGD(TAG, "NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); + ESP_LOGD(TAG, "NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); } void DebugComponent::update_platform_() {} -} // namespace debug -} // namespace esphome +} // namespace esphome::debug #endif diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index ddb8dbb1f0d..6981662d77e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -558,6 +558,7 @@ CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" +CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" # VFS requirement tracking # Components that need VFS features can call require_vfs_select() or require_vfs_dir() @@ -654,6 +655,9 @@ FRAMEWORK_SCHEMA = cv.All( ): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, + cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( + min=8192, max=32768 + ), } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( @@ -926,6 +930,10 @@ async def to_code(config): f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" ), ) + add_idf_sdkconfig_option( + "CONFIG_ARDUINO_LOOP_STACK_SIZE", + conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE], + ) add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) @@ -1071,6 +1079,10 @@ async def to_code(config): ) add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) + cg.add_define( + "ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE) + ) + cg.add_define( "USE_ESP_IDF_VERSION_CODE", cg.RawExpression( diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 1c8f29fa950..6215ff862f8 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -1,5 +1,6 @@ #ifdef USE_ESP32 +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -97,9 +98,9 @@ void loop_task(void *pv_params) { extern "C" void app_main() { esp32::setup_preferences(); #if CONFIG_FREERTOS_UNICORE - xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle); + xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle); #else - xTaskCreatePinnedToCore(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle, 1); + xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1); #endif } #endif // USE_ESP_IDF diff --git a/esphome/components/esp32_hosted/update/__init__.py b/esphome/components/esp32_hosted/update/__init__.py new file mode 100644 index 00000000000..040f989a646 --- /dev/null +++ b/esphome/components/esp32_hosted/update/__init__.py @@ -0,0 +1,78 @@ +import hashlib +from typing import Any + +import esphome.codegen as cg +from esphome.components import esp32, update +import esphome.config_validation as cv +from esphome.const import CONF_PATH, CONF_RAW_DATA_ID +from esphome.core import CORE, HexInt + +CODEOWNERS = ["@swoboda1337"] +AUTO_LOAD = ["sha256", "watchdog"] +DEPENDENCIES = ["esp32_hosted"] + +CONF_SHA256 = "sha256" + +esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted") +Esp32HostedUpdate = esp32_hosted_ns.class_( + "Esp32HostedUpdate", update.UpdateEntity, cg.Component +) + + +def _validate_sha256(value: Any) -> str: + value = cv.string_strict(value) + if len(value) != 64: + raise cv.Invalid("SHA256 must be 64 hexadecimal characters") + try: + bytes.fromhex(value) + except ValueError as e: + raise cv.Invalid(f"SHA256 must be valid hexadecimal: {e}") from e + return value + + +CONFIG_SCHEMA = cv.All( + update.update_schema(Esp32HostedUpdate, device_class="firmware").extend( + { + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + cv.Required(CONF_PATH): cv.file_, + cv.Required(CONF_SHA256): _validate_sha256, + } + ), + esp32.only_on_variant( + supported=[ + esp32.const.VARIANT_ESP32H2, + esp32.const.VARIANT_ESP32P4, + ] + ), +) + + +def _validate_firmware(config: dict[str, Any]) -> None: + path = CORE.relative_config_path(config[CONF_PATH]) + with open(path, "rb") as f: + firmware_data = f.read() + calculated = hashlib.sha256(firmware_data).hexdigest() + expected = config[CONF_SHA256].lower() + if calculated != expected: + raise cv.Invalid( + f"SHA256 mismatch for {config[CONF_PATH]}: expected {expected}, got {calculated}" + ) + + +FINAL_VALIDATE_SCHEMA = _validate_firmware + + +async def to_code(config: dict[str, Any]) -> None: + var = await update.new_update(config) + + path = config[CONF_PATH] + with open(CORE.relative_config_path(path), "rb") as f: + firmware_data = f.read() + rhs = [HexInt(x) for x in firmware_data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + + sha256_bytes = bytes.fromhex(config[CONF_SHA256]) + cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes])) + cg.add(var.set_firmware_data(prog_arr)) + cg.add(var.set_firmware_size(len(firmware_data))) + await cg.register_component(var, config) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp new file mode 100644 index 00000000000..adbcc5bf115 --- /dev/null +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -0,0 +1,164 @@ +#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) +#include "esp32_hosted_update.h" +#include "esphome/components/watchdog/watchdog.h" +#include "esphome/components/sha256/sha256.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include +#include +#include + +extern "C" { +#include +} + +namespace esphome::esp32_hosted { + +static const char *const TAG = "esp32_hosted.update"; + +// older coprocessor firmware versions have a 1500-byte limit per RPC call +constexpr size_t CHUNK_SIZE = 1500; + +void Esp32HostedUpdate::setup() { + this->update_info_.title = "ESP32 Hosted Coprocessor"; + + // get coprocessor version + esp_hosted_coprocessor_fwver_t ver_info; + if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { + this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1); + } else { + this->update_info_.current_version = "unknown"; + } + ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str()); + + // get image version + const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t); + if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) { + esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset); + if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) { + ESP_LOGD(TAG, "Firmware version: %s", app_desc->version); + ESP_LOGD(TAG, "Project name: %s", app_desc->project_name); + ESP_LOGD(TAG, "Build date: %s", app_desc->date); + ESP_LOGD(TAG, "Build time: %s", app_desc->time); + ESP_LOGD(TAG, "IDF version: %s", app_desc->idf_ver); + this->update_info_.latest_version = app_desc->version; + if (this->update_info_.latest_version != this->update_info_.current_version) { + this->state_ = update::UPDATE_STATE_AVAILABLE; + } else { + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + } else { + ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word, + ESP_APP_DESC_MAGIC_WORD); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + } else { + ESP_LOGW(TAG, "Firmware too small to contain app description"); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + + // publish state + this->status_clear_error(); + this->publish_state(); +} + +void Esp32HostedUpdate::dump_config() { + ESP_LOGCONFIG(TAG, + "ESP32 Hosted Update:\n" + " Current Version: %s\n" + " Latest Version: %s\n" + " Latest Size: %zu bytes", + this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(), + this->firmware_size_); +} + +void Esp32HostedUpdate::perform(bool force) { + if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) { + ESP_LOGW(TAG, "Update not available"); + return; + } + + if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) { + ESP_LOGE(TAG, "No firmware data available"); + return; + } + + sha256::SHA256 hasher; + hasher.init(); + hasher.add(this->firmware_data_, this->firmware_size_); + hasher.calculate(); + if (!hasher.equals_bytes(this->firmware_sha256_.data())) { + this->status_set_error("SHA256 verification failed"); + this->publish_state(); + return; + } + + ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_); + + watchdog::WatchdogManager watchdog(20000); + update::UpdateState prev_state = this->state_; + this->state_ = update::UPDATE_STATE_INSTALLING; + this->update_info_.has_progress = false; + this->publish_state(); + + esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error("Failed to begin OTA"); + this->publish_state(); + return; + } + + uint8_t chunk[CHUNK_SIZE]; + const uint8_t *data_ptr = this->firmware_data_; + size_t remaining = this->firmware_size_; + while (remaining > 0) { + size_t chunk_size = std::min(remaining, static_cast(CHUNK_SIZE)); + memcpy(chunk, data_ptr, chunk_size); + err = esp_hosted_slave_ota_write(chunk, chunk_size); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); + esp_hosted_slave_ota_end(); // NOLINT + this->state_ = prev_state; + this->status_set_error("Failed to write OTA data"); + this->publish_state(); + return; + } + data_ptr += chunk_size; + remaining -= chunk_size; + App.feed_wdt(); + } + + err = esp_hosted_slave_ota_end(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error("Failed to end OTA"); + this->publish_state(); + return; + } + + // activate new firmware + err = esp_hosted_slave_ota_activate(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error("Failed to activate OTA"); + this->publish_state(); + return; + } + + // update state + ESP_LOGI(TAG, "OTA update successful"); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + this->status_clear_error(); + this->publish_state(); + + // schedule a restart to ensure everything is in sync + ESP_LOGI(TAG, "Restarting in 1 second"); + this->set_timeout(1000, []() { App.safe_reboot(); }); +} + +} // namespace esphome::esp32_hosted +#endif diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.h b/esphome/components/esp32_hosted/update/esp32_hosted_update.h new file mode 100644 index 00000000000..9c087bf72aa --- /dev/null +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.h @@ -0,0 +1,32 @@ +#pragma once + +#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) + +#include "esphome/core/component.h" +#include "esphome/components/update/update_entity.h" +#include + +namespace esphome::esp32_hosted { + +class Esp32HostedUpdate : public update::UpdateEntity, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void perform(bool force) override; + void check() override {} + + void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; } + void set_firmware_size(size_t size) { this->firmware_size_ = size; } + void set_firmware_sha256(const std::array &sha256) { this->firmware_sha256_ = sha256; } + + protected: + const uint8_t *firmware_data_{nullptr}; + size_t firmware_size_{0}; + std::array firmware_sha256_; +}; + +} // namespace esphome::esp32_hosted + +#endif diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 3cfec1e94dc..63089237590 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -2,11 +2,18 @@ import logging from esphome import pins import esphome.codegen as cg +from esphome.components.zephyr import ( + zephyr_add_overlay, + zephyr_add_prj_conf, + zephyr_data, +) +from esphome.components.zephyr.const import KEY_BOARD from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, CONF_FREQUENCY, + CONF_I2C, CONF_I2C_ID, CONF_ID, CONF_SCAN, @@ -15,10 +22,12 @@ from esphome.const import ( CONF_TIMEOUT, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_NRF52, PLATFORM_RP2040, PlatformFramework, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj import esphome.final_validate as fv LOGGER = logging.getLogger(__name__) @@ -28,6 +37,7 @@ I2CBus = i2c_ns.class_("I2CBus") InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus) ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component) IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component) +ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component) I2CDevice = i2c_ns.class_("I2CDevice") @@ -41,6 +51,8 @@ def _bus_declare_type(value): return cv.declare_id(ArduinoI2CBus)(value) if CORE.using_esp_idf: return cv.declare_id(IDFI2CBus)(value) + if CORE.using_zephyr: + return cv.declare_id(ZephyrI2CBus)(value) raise NotImplementedError @@ -62,23 +74,70 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), - cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All( - cv.frequency, cv.Range(min=0, min_included=False) + cv.SplitDefault( + CONF_FREQUENCY, + esp32="50kHz", + esp8266="50kHz", + rp2040="50kHz", + nrf52="100kHz", + ): cv.All( + cv.frequency, + cv.Range(min=0, min_included=False), + ), + cv.Optional(CONF_TIMEOUT): cv.All( + cv.only_with_framework(["arduino", "esp-idf"]), + cv.positive_time_period, ), - cv.Optional(CONF_TIMEOUT): cv.positive_time_period, cv.Optional(CONF_SCAN, default=True): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_NRF52]), validate_config, ) +def _final_validate(config): + full_config = fv.full_config.get()[CONF_I2C] + if CORE.using_zephyr and len(full_config) > 1: + raise cv.Invalid("Second i2c is not implemented on Zephyr yet") + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.BUS) async def to_code(config): cg.add_global(i2c_ns.using) cg.add_define("USE_I2C") - var = cg.new_Pvariable(config[CONF_ID]) + if CORE.using_zephyr: + zephyr_add_prj_conf("I2C", True) + i2c = "i2c0" + if zephyr_data()[KEY_BOARD] in ["xiao_ble"]: + i2c = "i2c1" + zephyr_add_overlay( + f""" + &pinctrl {{ + {i2c}_default: {i2c}_default {{ + group1 {{ + psels = , + ; + }}; + }}; + {i2c}_sleep: {i2c}_sleep {{ + group1 {{ + psels = , + ; + low-power-enable; + }}; + }}; + }}; + """ + ) + var = cg.new_Pvariable( + config[CONF_ID], MockObj(f"DEVICE_DT_GET(DT_NODELABEL({i2c}))") + ) + else: + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) cg.add(var.set_sda_pin(config[CONF_SDA])) @@ -197,5 +256,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.LN882X_ARDUINO, }, "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 31c21f398cb..f8c7a1b40bd 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -1,6 +1,7 @@ #include "i2c.h" #include "esphome/core/defines.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include @@ -23,6 +24,8 @@ void I2CBus::i2c_scan_() { } else if (err == ERROR_UNKNOWN) { scan_results_.emplace_back(address, false); } + // it takes 16sec to scan on nrf52. It prevents board reset. + arch_feed_wdt(); } #if defined(USE_ESP32) && defined(USE_LOGGER) esp_log_level_set("*", previous); diff --git a/esphome/components/i2c/i2c_bus_zephyr.cpp b/esphome/components/i2c/i2c_bus_zephyr.cpp new file mode 100644 index 00000000000..658dcee35ce --- /dev/null +++ b/esphome/components/i2c/i2c_bus_zephyr.cpp @@ -0,0 +1,133 @@ +#ifdef USE_ZEPHYR + +#include "i2c_bus_zephyr.h" +#include +#include "esphome/core/log.h" + +namespace esphome::i2c { + +static const char *const TAG = "i2c.zephyr"; + +void ZephyrI2CBus::setup() { + if (!device_is_ready(this->i2c_dev_)) { + ESP_LOGE(TAG, "I2C dev is not ready."); + mark_failed(); + return; + } + + int ret = i2c_configure(this->i2c_dev_, this->dev_config_); + if (ret < 0) { + ESP_LOGE(TAG, "I2C: Failed to configure device"); + } + + this->recovery_result_ = i2c_recover_bus(this->i2c_dev_); + if (this->recovery_result_ != 0) { + ESP_LOGE(TAG, "I2C recover bus failed, err %d", this->recovery_result_); + } + if (this->scan_) { + ESP_LOGV(TAG, "Scanning I2C bus for active devices..."); + this->i2c_scan_(); + } +} + +void ZephyrI2CBus::dump_config() { + auto get_speed = [](uint32_t dev_config) { + switch (I2C_SPEED_GET(dev_config)) { + case I2C_SPEED_STANDARD: + return "100 kHz"; + case I2C_SPEED_FAST: + return "400 kHz"; + case I2C_SPEED_FAST_PLUS: + return "1 MHz"; + case I2C_SPEED_HIGH: + return "3.4 MHz"; + case I2C_SPEED_ULTRA: + return "5 MHz"; + } + return "unknown"; + }; + ESP_LOGCONFIG(TAG, + "I2C Bus:\n" + " SDA Pin: GPIO%u\n" + " SCL Pin: GPIO%u\n" + " Frequency: %s\n" + " Name: %s", + this->sda_pin_, this->scl_pin_, get_speed(this->dev_config_), this->i2c_dev_->name); + + if (this->recovery_result_ != 0) { + ESP_LOGCONFIG(TAG, " Recovery: failed, err %d", this->recovery_result_); + } else { + ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); + } + if (this->scan_) { + ESP_LOGI(TAG, "Results from I2C bus scan:"); + if (scan_results_.empty()) { + ESP_LOGI(TAG, "Found no I2C devices!"); + } else { + for (const auto &s : scan_results_) { + if (s.second) { + ESP_LOGI(TAG, "Found I2C device at address 0x%02X", s.first); + } else { + ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); + } + } + } + } +} + +ErrorCode ZephyrI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { + if (!device_is_ready(this->i2c_dev_)) { + return ERROR_NOT_INITIALIZED; + } + + i2c_msg msgs[2]{}; + size_t cnt = 0; + uint8_t dst = 0x00; // dummy data to not use random value + + if (read_count == 0 && write_count == 0) { + msgs[cnt].buf = &dst; + msgs[cnt].len = 0U; + msgs[cnt++].flags = I2C_MSG_WRITE; + } else { + if (write_count) { + // the same struct is used for read/write — const cast is fine; data isn't modified + msgs[cnt].buf = const_cast(write_buffer); + msgs[cnt].len = write_count; + msgs[cnt++].flags = I2C_MSG_WRITE; + } + if (read_count) { + msgs[cnt].buf = const_cast(read_buffer); + msgs[cnt].len = read_count; + msgs[cnt++].flags = I2C_MSG_READ | I2C_MSG_RESTART; + } + } + + msgs[cnt - 1].flags |= I2C_MSG_STOP; + + auto err = i2c_transfer(this->i2c_dev_, msgs, cnt, address); + + if (err == -EIO) { + return ERROR_NOT_ACKNOWLEDGED; + } + + if (err != 0) { + ESP_LOGE(TAG, "i2c transfer error %d", err); + return ERROR_UNKNOWN; + } + + return ERROR_OK; +} + +void ZephyrI2CBus::set_frequency(uint32_t frequency) { + this->dev_config_ &= ~I2C_SPEED_MASK; + if (frequency >= 400000) { + this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_FAST); + } else { + this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_STANDARD); + } +} + +} // namespace esphome::i2c + +#endif diff --git a/esphome/components/i2c/i2c_bus_zephyr.h b/esphome/components/i2c/i2c_bus_zephyr.h new file mode 100644 index 00000000000..49cac5b9921 --- /dev/null +++ b/esphome/components/i2c/i2c_bus_zephyr.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef USE_ZEPHYR + +#include "i2c_bus.h" +#include "esphome/core/component.h" + +struct device; + +namespace esphome::i2c { + +class ZephyrI2CBus : public InternalI2CBus, public Component { + public: + explicit ZephyrI2CBus(const device *i2c_dev) : i2c_dev_(i2c_dev) {} + void setup() override; + void dump_config() override; + ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_scan(bool scan) { scan_ = scan; } + void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; } + void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; } + void set_frequency(uint32_t frequency); + + int get_port() const override { return 0; } + + protected: + const device *i2c_dev_; + int recovery_result_ = 0; + uint8_t sda_pin_{}; + uint8_t scl_pin_{}; + uint32_t dev_config_{}; +}; + +} // namespace esphome::i2c + +#endif diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 8cd71153681..575fb97799b 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone +from esphome.components import esp32, microphone, socket import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kahrendt", "@jesserockz"] DEPENDENCIES = ["microphone"] +AUTO_LOAD = ["socket"] DOMAIN = "micro_wake_word" @@ -443,6 +444,10 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Enable wake_loop_threadsafe() for low-latency wake word detection + # The inference task queues detection events that need immediate processing + socket.require_wake_loop_threadsafe() + mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) cg.add(var.set_microphone_source(mic_source)) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 6fca48a5bd3..a0547b158ef 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -2,6 +2,7 @@ #ifdef USE_ESP_IDF +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -426,6 +427,12 @@ void MicroWakeWord::process_probabilities_() { if (vad_state.detected) { #endif xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY); + + // Wake main loop immediately to process wake word detection +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + model->reset_probabilities(); #ifdef USE_MICRO_WAKE_WORD_VAD } else { diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 6685a23c411..0d8340f95fb 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -72,7 +72,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF; if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL && - static_cast(hardware_id) != ETRAILER) { + static_cast(hardware_id) != ETRAILER && static_cast(hardware_id) != STANDARD_ALT) { ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); return false; } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index b92445df34b..897b5414ed0 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -15,6 +15,7 @@ namespace mopeka_std_check { enum SensorType { STANDARD = 0x02, XL = 0x03, + STANDARD_ALT = 0x44, ETRAILER = 0x46, }; diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index a6f4e0a2018..aee2b389421 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -357,8 +357,8 @@ bool MQTTClimateComponent::publish_state_() { payload = "unknown"; } } - if (this->device_->custom_preset.has_value()) - payload = this->device_->custom_preset.value(); + if (this->device_->has_custom_preset()) + payload = this->device_->get_custom_preset(); if (!this->publish(this->get_preset_state_topic(), payload)) success = false; } @@ -429,8 +429,8 @@ bool MQTTClimateComponent::publish_state_() { payload = "unknown"; } } - if (this->device_->custom_fan_mode.has_value()) - payload = this->device_->custom_fan_mode.value(); + if (this->device_->has_custom_fan_mode()) + payload = this->device_->get_custom_fan_mode(); if (!this->publish(this->get_fan_mode_state_topic(), payload)) success = false; } diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index f7e9872ce10..af662a05a00 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -42,23 +42,21 @@ static const LogString *sensor_type_to_string(AlarmSensorType type) { #endif void TemplateAlarmControlPanel::dump_config() { - ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:"); ESP_LOGCONFIG(TAG, + "TemplateAlarmControlPanel:\n" " Current State: %s\n" - " Number of Codes: %u", - LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size()); - if (!this->codes_.empty()) - ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->requires_code_to_arm_)); - ESP_LOGCONFIG(TAG, " Arming Away Time: %" PRIu32 "s", (this->arming_away_time_ / 1000)); - if (this->arming_home_time_ != 0) - ESP_LOGCONFIG(TAG, " Arming Home Time: %" PRIu32 "s", (this->arming_home_time_ / 1000)); - if (this->arming_night_time_ != 0) - ESP_LOGCONFIG(TAG, " Arming Night Time: %" PRIu32 "s", (this->arming_night_time_ / 1000)); - ESP_LOGCONFIG(TAG, + " Number of Codes: %u\n" + " Requires Code To Arm: %s\n" + " Arming Away Time: %" PRIu32 "s\n" + " Arming Home Time: %" PRIu32 "s\n" + " Arming Night Time: %" PRIu32 "s\n" " Pending Time: %" PRIu32 "s\n" " Trigger Time: %" PRIu32 "s\n" " Supported Features: %" PRIu32, - (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); + LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size(), + YESNO(!this->codes_.empty() && this->requires_code_to_arm_), (this->arming_away_time_ / 1000), + (this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000), + (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR for (auto const &[sensor, info] : this->sensor_map_) { ESP_LOGCONFIG(TAG, diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index a2fb12a5e22..0381fbcba9d 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -1,4 +1,5 @@ from pathlib import Path +import textwrap from typing import TypedDict import esphome.codegen as cg @@ -90,7 +91,7 @@ def zephyr_add_prj_conf( def zephyr_add_overlay(content): - zephyr_data()[KEY_OVERLAY] += content + zephyr_data()[KEY_OVERLAY] += textwrap.dedent(content) def add_extra_build_file(filename: str, path: Path) -> bool: diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 65069f5b2f8..2be32058ea0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -155,6 +155,7 @@ // IDF-specific feature flags #ifdef USE_ESP_IDF #define USE_MQTT_IDF_ENQUEUE +#define ESPHOME_LOOP_TASK_STACK_SIZE 8192 #endif // ESP32-specific feature flags diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 1a6dc8b97d3..31112caf0ad 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -6,15 +6,15 @@ dependencies: espressif/mdns: version: 1.8.2 espressif/esp_wifi_remote: - version: 0.10.2 + version: 1.1.5 rules: - if: "target in [esp32h2, esp32p4]" espressif/eppp_link: - version: 0.2.0 + version: 1.1.3 rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.0.11 + version: 2.6.1 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: diff --git a/script/ci-custom.py b/script/ci-custom.py index 6b01623d92c..106aa438fe7 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -71,6 +71,7 @@ ignore_types = ( ".apng", ".gif", ".webp", + ".bin", ) LINT_FILE_CHECKS = [] diff --git a/tests/components/ads1115/test.nrf52-adafruit.yaml b/tests/components/ads1115/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/ads1115/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/ads1115/test.nrf52-mcumgr.yaml b/tests/components/ads1115/test.nrf52-mcumgr.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/ads1115/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/aht10/test.nrf52-adafruit.yaml b/tests/components/aht10/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/aht10/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/esp32/test-stack_size.esp32-idf.yaml b/tests/components/esp32/test-stack_size.esp32-idf.yaml new file mode 100644 index 00000000000..49535880352 --- /dev/null +++ b/tests/components/esp32/test-stack_size.esp32-idf.yaml @@ -0,0 +1,6 @@ +esp32: + board: esp32dev + framework: + type: esp-idf + advanced: + loop_task_stack_size: 16384 diff --git a/tests/components/esp32_hosted/.gitattributes b/tests/components/esp32_hosted/.gitattributes new file mode 100644 index 00000000000..6abdc561178 --- /dev/null +++ b/tests/components/esp32_hosted/.gitattributes @@ -0,0 +1 @@ +*.bin -text diff --git a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml index dade44d145b..2a76f17e2fb 100644 --- a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml +++ b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml @@ -1 +1,7 @@ <<: !include common.yaml + +update: + - platform: esp32_hosted + name: "Coprocessor Firmware Update" + path: $component_dir/test_firmware.bin + sha256: de2f256064a0af797747c2b97505dc0b9f3df0de4f489eac731c23ae9ca9cc31 diff --git a/tests/components/esp32_hosted/test_firmware.bin b/tests/components/esp32_hosted/test_firmware.bin new file mode 100644 index 00000000000..c97c12f9b0a Binary files /dev/null and b/tests/components/esp32_hosted/test_firmware.bin differ diff --git a/tests/components/i2c/test.nrf52-adafruit.yaml b/tests/components/i2c/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/i2c/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-mcumgr.yaml b/tests/components/i2c/test.nrf52-mcumgr.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/i2c/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-xiao-ble.yaml b/tests/components/i2c/test.nrf52-xiao-ble.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/i2c/test.nrf52-xiao-ble.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/test_build_components/common/i2c/nrf52.yaml b/tests/test_build_components/common/i2c/nrf52.yaml new file mode 100644 index 00000000000..b86cdf0d693 --- /dev/null +++ b/tests/test_build_components/common/i2c/nrf52.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for NRF52 tests + +substitutions: + scl_pin: P0.04 + sda_pin: P0.05 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true