mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Merge remote-tracking branch 'upstream/dev' into HEAD
This commit is contained in:
@@ -90,9 +90,10 @@ def get_project_cmakelists(
|
||||
"""
|
||||
idf_target = variant_to_idf_target(get_esp32_variant())
|
||||
|
||||
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
|
||||
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
|
||||
# --format=raw because the legacy mode doesn't support it.
|
||||
# esp_idf_size 2.x (IDF >=6.0) made NG the default and removed --ng;
|
||||
# 1.x (IDF 5.5) needs --ng for --format=json2. 1.x json2 also lacks
|
||||
# total_size, hence the ELF fallback in espidf/size_summary.py; both
|
||||
# go away together when 1.x support is dropped.
|
||||
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
|
||||
|
||||
# Project-wide compile options: -D defines and -W warning flags (skip
|
||||
@@ -211,10 +212,12 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
|
||||
project({CORE.name})
|
||||
|
||||
# Emit raw JSON size data for ESPHome to read post-build.
|
||||
# Emit per-memory-type JSON size data for ESPHome to read post-build.
|
||||
# json2 stays small; raw dumps every symbol (~2s on a large map) and
|
||||
# this command runs inside the link edge, blocking everything downstream.
|
||||
add_custom_command(
|
||||
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
|
||||
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
|
||||
${{CMAKE_PROJECT_NAME}}.map
|
||||
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
|
||||
|
||||
@@ -339,7 +339,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# HTTPS streams verify the server against the root certificate bundle
|
||||
require_certificate_bundle()
|
||||
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.0")
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
|
||||
|
||||
data = _get_data()
|
||||
|
||||
|
||||
@@ -17,11 +17,7 @@ class BinaryLightOutput final : public light::LightOutput {
|
||||
void write_state(light::LightState *state) override {
|
||||
bool binary;
|
||||
state->current_values_as_binary(&binary);
|
||||
if (binary) {
|
||||
this->output_->turn_on();
|
||||
} else {
|
||||
this->output_->turn_off();
|
||||
}
|
||||
this->output_->set_state(binary);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -626,7 +626,7 @@ void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) {
|
||||
// explicit kick the MTU would only be exchanged on the first GATT query,
|
||||
// which never happens on a V3_WITH_CACHE connection.
|
||||
// Both registration calls above return void (BTstack 075a078, arduino-pico
|
||||
// 6.0.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
|
||||
// 6.1.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
|
||||
// the connect timeout in loop().
|
||||
gatt_client_send_mtu_negotiation(&RP2GattClient::gatt_packet_handler, this->con_handle_);
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
|
||||
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
|
||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_FLASH_CHIP = "flash_chip"
|
||||
CONF_KEY_ID = "key_id"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
CONF_NVS_ENCRYPTION = "nvs_encryption"
|
||||
@@ -464,6 +465,20 @@ ESP32_CHIP_REVISIONS = {
|
||||
"3.1": "CONFIG_ESP32_REV_MIN_3_1",
|
||||
}
|
||||
|
||||
# Flash vendor drivers ESP-IDF can link; each costs IRAM plus a 124 B table in DRAM
|
||||
# and only the one matching the flash ID is ever used
|
||||
ESP32_FLASH_CHIPS = {
|
||||
"gd": "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP",
|
||||
"issi": "CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP",
|
||||
"mxic": "CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP",
|
||||
"winbond": "CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP",
|
||||
"boya": "CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP",
|
||||
"th": "CONFIG_SPI_FLASH_SUPPORT_TH_CHIP",
|
||||
"mxic_opi": "CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP",
|
||||
}
|
||||
FLASH_CHIP_GENERIC = "generic"
|
||||
FLASH_CHIP_OPI = "mxic_opi" # the octal driver, ESP32-S3 only
|
||||
|
||||
# Socket limit configuration for ESP-IDF
|
||||
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
|
||||
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
|
||||
@@ -1533,6 +1548,25 @@ def final_validate(config) -> None:
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM],
|
||||
)
|
||||
)
|
||||
if (flash_chip := advanced.get(CONF_FLASH_CHIP)) is not None:
|
||||
opi = flash_chip == FLASH_CHIP_OPI
|
||||
if opi and config[CONF_VARIANT] != VARIANT_ESP32S3:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_FLASH_CHIP}: {flash_chip}' is only supported on {VARIANT_ESP32S3}",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
|
||||
)
|
||||
)
|
||||
elif opi != (config.get(CONF_FLASH_MODE) == "opi"):
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_FLASH_CHIP}: {flash_chip}' requires '{CONF_FLASH_MODE}: opi'"
|
||||
if opi
|
||||
else f"'{CONF_FLASH_CHIP}: {flash_chip}' does not match "
|
||||
f"'{CONF_FLASH_MODE}: opi'; octal flash uses {FLASH_CHIP_OPI}",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
|
||||
)
|
||||
)
|
||||
if (
|
||||
config[CONF_VARIANT] != VARIANT_ESP32P4
|
||||
and config.get(CONF_ENGINEERING_SAMPLE) is not None
|
||||
@@ -1971,6 +2005,9 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
*ESP32_CHIP_REVISIONS, string=True
|
||||
),
|
||||
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_FLASH_CHIP): cv.one_of(
|
||||
FLASH_CHIP_GENERIC, *ESP32_FLASH_CHIPS, lower=True
|
||||
),
|
||||
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
|
||||
# it will handle disabling DHCP server when AP is not configured.
|
||||
# Default to false (disabled) when WiFi is not used.
|
||||
@@ -2765,6 +2802,11 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option(flag, rev == min_rev)
|
||||
cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET")
|
||||
|
||||
# Keep only the flash vendor driver the board needs; the boot log names it
|
||||
if (flash_chip := conf[CONF_ADVANCED].get(CONF_FLASH_CHIP)) is not None:
|
||||
for chip, flag in ESP32_FLASH_CHIPS.items():
|
||||
add_idf_sdkconfig_option(flag, chip == flash_chip)
|
||||
|
||||
# Use SRAM1 region as IRAM on ESP32 (original) variant
|
||||
# This provides an additional 40KB of IRAM by using SRAM1 memory that was previously
|
||||
# reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later.
|
||||
|
||||
@@ -173,7 +173,10 @@ static const char *const TAG = "esp32.crash";
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME);
|
||||
|
||||
void crash_handler_read_and_clear() {
|
||||
// Validate the NOINIT record. Runs on every has_data() call; re-running is
|
||||
// harmless and the magic is left alone so the record survives an OTA
|
||||
// rollback reboot, crash_handler_clear() drops it once an API client has it.
|
||||
static void read_crash_data() {
|
||||
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
|
||||
s_crash_data_valid = true;
|
||||
// Clamp counts to prevent out-of-bounds reads from corrupt .noinit data
|
||||
@@ -194,11 +197,12 @@ void crash_handler_read_and_clear() {
|
||||
s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count;
|
||||
#endif
|
||||
}
|
||||
// Don't clear magic here — crash data must survive OTA rollback reboots.
|
||||
// Magic is cleared by crash_handler_clear() after an API client receives the data.
|
||||
}
|
||||
|
||||
bool crash_handler_has_data() { return s_crash_data_valid; }
|
||||
bool crash_handler_has_data() {
|
||||
read_crash_data();
|
||||
return s_crash_data_valid;
|
||||
}
|
||||
|
||||
void crash_handler_clear() {
|
||||
// Only clear the magic so data doesn't survive the next reboot.
|
||||
@@ -426,7 +430,7 @@ static void log_foreign_addresses() {
|
||||
// crashes again during boot, and allowing the CLI's process_stacktrace to match
|
||||
// and decode each address individually.
|
||||
void crash_handler_log() {
|
||||
if (!s_crash_data_valid)
|
||||
if (!crash_handler_has_data())
|
||||
return;
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
/// Read and validate crash data from NOINIT memory.
|
||||
/// Does not clear the magic marker — call crash_handler_clear() after
|
||||
/// the data has been delivered to an API client so it survives OTA rollback reboots.
|
||||
void crash_handler_read_and_clear();
|
||||
|
||||
/// Log crash data if a crash was detected on previous boot.
|
||||
void crash_handler_log();
|
||||
|
||||
@@ -16,7 +11,7 @@ void crash_handler_log();
|
||||
/// Call after the data has been delivered to an API client.
|
||||
void crash_handler_clear();
|
||||
|
||||
/// Returns true if crash data was found this boot.
|
||||
/// Returns true if crash data was found this boot, reading it first if needed.
|
||||
bool crash_handler_has_data();
|
||||
|
||||
} // namespace esphome::esp32
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
// defines.h must come before crash_handler.h so USE_ESP32_CRASH_HANDLER is set
|
||||
// before crash_handler.h's #ifdef-guarded namespace block is parsed.
|
||||
#include "esphome/core/defines.h"
|
||||
#include "crash_handler.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <esp_clk_tree.h>
|
||||
@@ -45,11 +42,6 @@ void arch_restart() {
|
||||
}
|
||||
|
||||
void arch_init() {
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
// Read crash data from previous boot before anything else
|
||||
esp32::crash_handler_read_and_clear();
|
||||
#endif
|
||||
|
||||
// Enable the task watchdog only on the loop task (from which we're currently running)
|
||||
esp_task_wdt_add(nullptr);
|
||||
|
||||
|
||||
@@ -48,14 +48,12 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
continue; // skip drawing
|
||||
}
|
||||
break;
|
||||
case TRANSPARENCY_ALPHA_CHANNEL: {
|
||||
auto on = (float) gray / 255.0f;
|
||||
auto off = 1.0f - on;
|
||||
// blend color_on and color_off
|
||||
color = Color(color_on.r * on + color_off.r * off, color_on.g * on + color_off.g * off,
|
||||
color_on.b * on + color_off.b * off, 0xFF);
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
// gray is the alpha: blend from color_off to color_on, drawn opaque
|
||||
color = Color(Color::blend_channel(color_off.r, color_on.r, gray),
|
||||
Color::blend_channel(color_off.g, color_on.g, gray),
|
||||
Color::blend_channel(color_off.b, color_on.b, gray), 0xFF);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -208,11 +208,7 @@ void ImprovBLEComponent::set_status_indicator_state_(bool state) {
|
||||
if (this->status_indicator_state_ == state)
|
||||
return;
|
||||
this->status_indicator_state_ = state;
|
||||
if (state) {
|
||||
this->status_indicator_->turn_on();
|
||||
} else {
|
||||
this->status_indicator_->turn_off();
|
||||
}
|
||||
this->status_indicator_->set_state(state);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape)
|
||||
}
|
||||
|
||||
JsonBuilder::JsonBuilder() = default;
|
||||
|
||||
SerializationBuffer<> JsonBuilder::serialize() {
|
||||
// ===========================================================================================
|
||||
// CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING
|
||||
|
||||
@@ -168,6 +168,9 @@ inline JsonDocument parse_json(const std::string &data) {
|
||||
/// Builder class for creating JSON documents without lambdas
|
||||
class JsonBuilder {
|
||||
public:
|
||||
// Out of line: inlining the JsonDocument constructor duplicates it at every call site
|
||||
JsonBuilder();
|
||||
|
||||
JsonObject root() {
|
||||
if (!root_created_) {
|
||||
root_ = doc_.to<JsonObject>();
|
||||
|
||||
@@ -38,14 +38,6 @@ float Mcp4461Wiper::update_state() {
|
||||
return this->state_;
|
||||
}
|
||||
|
||||
void Mcp4461Wiper::set_state(bool state) {
|
||||
if (state) {
|
||||
this->turn_on();
|
||||
} else {
|
||||
this->turn_off();
|
||||
}
|
||||
}
|
||||
|
||||
void Mcp4461Wiper::turn_on() { this->parent_->enable_wiper_(this->wiper_); }
|
||||
|
||||
void Mcp4461Wiper::turn_off() { this->parent_->disable_wiper_(this->wiper_); }
|
||||
|
||||
@@ -13,9 +13,6 @@ class Mcp4461Wiper final : public output::FloatOutput, public Parented<Mcp4461Co
|
||||
/// @brief Set level of wiper
|
||||
/// @param[in] state - The desired float level in range 0-1.0
|
||||
void set_level(float state);
|
||||
/// @brief Enables/Disables current output using bool parameter
|
||||
/// @param[in] state boolean var representing desired state (true=ON, false=OFF)
|
||||
void set_state(bool state) override;
|
||||
/// @brief Enables current output
|
||||
void turn_on() override;
|
||||
/// @brief Disables current output
|
||||
|
||||
@@ -385,7 +385,8 @@ void MixerSpeaker::loop() {
|
||||
// Retries on a subsequent loop if the task is still running on the other core
|
||||
if ((event_group_bits & MIXER_TASK_STATE_STOPPED) && this->task_.deallocate()) {
|
||||
ESP_LOGD(TAG, "Stopped");
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
|
||||
// Keep a start request that arrived while the task was stopping, otherwise it is lost for good
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS & ~MIXER_TASK_COMMAND_START);
|
||||
this->all_stopped_since_ms_ = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,53 +30,10 @@ using modbus::ModbusFunctionCode;
|
||||
using modbus::ModbusRegisterType;
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
// Remove before 2026.10.0 — these helpers have moved to modbus::helpers
|
||||
ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); }
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline FunctionCode modbus_register_read_function(modbus::EntityType reg_type) {
|
||||
return modbus::helpers::modbus_register_read_function(reg_type);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline FunctionCode modbus_register_write_function(modbus::EntityType reg_type) {
|
||||
return modbus::helpers::modbus_register_write_function(reg_type);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::c_to_hex() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint8_t c_to_hex(char c) { return modbus::helpers::c_to_hex(c); }
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::byte_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::byte_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::word_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::word_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::dword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::dword_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::qword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return modbus::helpers::qword_from_hex_str(value, pos);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
|
||||
return modbus::helpers::get_data<T>(data, buffer_offset);
|
||||
}
|
||||
|
||||
// Span overloads of the deprecated helpers below: read lambdas receive their payload as a
|
||||
// Span overloads of the former modbus_controller helpers: read lambdas receive their payload as a
|
||||
// std::span<const uint8_t> (previously a const std::vector<uint8_t> &), and a span does not convert to
|
||||
// a vector, so existing lambdas calling these by name need an overload that accepts one. These carry
|
||||
// this release's deprecation window, since the span forms only exist from it.
|
||||
// the 2026.8.0 deprecation window, since the span forms only exist from it.
|
||||
// payload_to_number() deliberately has no such overload: one of its arguments is a modbus::helpers
|
||||
// type, so a span call already reaches the helper by argument-dependent lookup, and a forwarder here
|
||||
// would only make that call ambiguous.
|
||||
@@ -99,33 +56,6 @@ inline bool coil_from_vector(int coil, std::span<const uint8_t> data) {
|
||||
return modbus::helpers::bit_from_packed(coil, data);
|
||||
}
|
||||
|
||||
template<typename N>
|
||||
ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
N mask_and_shift_by_rightbit(N data, uint32_t mask) {
|
||||
return modbus::helpers::mask_and_shift_by_rightbit(data, mask);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
|
||||
modbus::helpers::number_to_payload(data, value, value_type);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
|
||||
uint32_t bitmask) {
|
||||
return modbus::helpers::payload_to_number(std::span<const uint8_t>(data), sensor_value_type, offset, bitmask)
|
||||
.value_or(0);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
|
||||
inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
|
||||
std::vector<uint16_t> data;
|
||||
modbus::helpers::float_to_payload(data, value, value_type);
|
||||
return data;
|
||||
}
|
||||
|
||||
class ModbusController;
|
||||
|
||||
/// How an item relates to the register range built just before it (same register type, address order).
|
||||
/// The numeric order doubles as the comparator tiebreak for items at the same address (see
|
||||
/// SensorItemsComparator): AUTO items form the shared range first, so a NEVER item comes last and
|
||||
|
||||
@@ -23,7 +23,6 @@ void ModbusNumber::parse_and_publish(std::span<const uint8_t> data) {
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Number new state : %.02f", result);
|
||||
// this->sensor_->raw_state = result;
|
||||
this->publish_state(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ void ModbusSensor::parse_and_publish(std::span<const uint8_t> data) {
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Sensor new state: %.02f", result);
|
||||
// this->sensor_->raw_state = result;
|
||||
this->publish_state(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,7 @@ static const char *const TAG = "output.switch";
|
||||
void OutputSwitch::dump_config() { LOG_SWITCH("", "Output Switch", this); }
|
||||
void OutputSwitch::setup() { this->control(this->get_initial_state_with_restore_mode().value_or(false)); }
|
||||
void OutputSwitch::write_state(bool state) {
|
||||
if (state) {
|
||||
this->output_->turn_on();
|
||||
} else {
|
||||
this->output_->turn_off();
|
||||
}
|
||||
this->output_->set_state(state);
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,20 +112,21 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
|
||||
cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema),
|
||||
cv.Optional(CONF_DUMP, default=[]): remote_base.validate_dumpers,
|
||||
cv.Optional(CONF_TOLERANCE, default="25%"): validate_tolerance,
|
||||
# pulse ring targets hold one 4 byte entry per pulse; 4000b keeps their 1000 pulses
|
||||
cv.SplitDefault(
|
||||
CONF_BUFFER_SIZE,
|
||||
esp32=cv.UNDEFINED,
|
||||
# the pulse ring needs a size; only RMT targets size themselves in setup()
|
||||
**{
|
||||
f"esp32_{variant.removeprefix('ESP32').lower()}": "1000b"
|
||||
f"esp32_{variant.removeprefix('ESP32').lower()}": "4000b"
|
||||
for variant in esp32_rmt.VARIANTS_NO_RMT
|
||||
},
|
||||
esp8266="1000b",
|
||||
bk72xx="1000b",
|
||||
ln882x="1000b",
|
||||
rtl87xx="1000b",
|
||||
rp2="1000b",
|
||||
): cv.All(cv.validate_bytes, cv.int_range(min=64)),
|
||||
esp8266="4000b",
|
||||
bk72xx="4000b",
|
||||
ln882x="4000b",
|
||||
rtl87xx="4000b",
|
||||
rp2="4000b",
|
||||
): cv.All(cv.validate_bytes, cv.int_range(min=64, max=65535)),
|
||||
cv.Optional(CONF_FILTER, default="50us"): cv.All(
|
||||
cv.positive_time_period_microseconds,
|
||||
cv.Range(max=TimePeriod(microseconds=4294967295)),
|
||||
|
||||
@@ -14,7 +14,7 @@ static void IRAM_ATTR HOT write_value(RemoteReceiverComponentStore *arg, uint32_
|
||||
int32_t multiplier = ((int32_t) level << 1) - 1;
|
||||
uint32_t buffer_write = arg->buffer_write;
|
||||
arg->buffer[buffer_write++] = (int32_t) delta * multiplier;
|
||||
if (buffer_write >= arg->buffer_size) {
|
||||
if (buffer_write >= arg->buffer_entries) {
|
||||
buffer_write = 0;
|
||||
}
|
||||
|
||||
@@ -65,8 +65,9 @@ void RemoteReceiverComponent::setup() {
|
||||
this->store_.idle_us = this->idle_us_;
|
||||
this->store_.filter_us = this->filter_us_;
|
||||
this->store_.pin = this->pin_->to_isr();
|
||||
this->store_.buffer = new int32_t[this->buffer_size_];
|
||||
this->store_.buffer_size = this->buffer_size_;
|
||||
// rounded up so a size that is not a multiple of four never holds less than requested
|
||||
this->store_.buffer_entries = (this->buffer_size_ + sizeof(int32_t) - 1) / sizeof(int32_t);
|
||||
this->store_.buffer = new int32_t[this->store_.buffer_entries];
|
||||
this->store_.prev_micros = micros();
|
||||
this->store_.commit_micros = this->store_.prev_micros;
|
||||
this->store_.prev_level = this->pin_->digital_read();
|
||||
@@ -79,11 +80,11 @@ void RemoteReceiverComponent::dump_config() {
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
"Remote Receiver:\n"
|
||||
" Buffer Size: %" PRIu32 "\n"
|
||||
" Buffer Size: %" PRIu32 " bytes (%" PRIu32 " pulses)\n"
|
||||
" Tolerance: %" PRIu32 "%s\n"
|
||||
" Filter out pulses shorter than: %" PRIu32 " us\n"
|
||||
" Signal is done after %" PRIu32 " us of no changes",
|
||||
this->buffer_size_, this->tolerance_,
|
||||
this->buffer_size_, this->store_.buffer_entries, this->tolerance_,
|
||||
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"),
|
||||
this->filter_us_, this->idle_us_);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
@@ -119,7 +120,7 @@ void RemoteReceiverComponent::loop() {
|
||||
while (temp_read != last_index && (uint32_t) std::abs(s.buffer[temp_read]) < this->idle_us_) {
|
||||
reserve_size++;
|
||||
temp_read++;
|
||||
if (temp_read >= s.buffer_size) {
|
||||
if (temp_read >= s.buffer_entries) {
|
||||
temp_read = 0;
|
||||
}
|
||||
}
|
||||
@@ -129,7 +130,7 @@ void RemoteReceiverComponent::loop() {
|
||||
// read the buffer
|
||||
for (uint32_t i = 0; i < reserve_size + 1; i++) {
|
||||
this->temp_.push_back((int32_t) s.buffer[s.buffer_read++]);
|
||||
if (s.buffer_read >= s.buffer_size) {
|
||||
if (s.buffer_read >= s.buffer_entries) {
|
||||
s.buffer_read = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ struct RemoteReceiverComponentStore {
|
||||
uint32_t buffer_read{0};
|
||||
volatile uint32_t commit_micros{0};
|
||||
volatile uint32_t prev_micros{0};
|
||||
uint32_t buffer_size{1000};
|
||||
uint32_t buffer_entries{0};
|
||||
uint32_t filter_us{10};
|
||||
uint32_t idle_us{10000};
|
||||
ISRInternalGPIOPin pin;
|
||||
@@ -83,14 +83,14 @@ class RemoteReceiverComponent final : public remote_base::RemoteReceiverBase,
|
||||
protected:
|
||||
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||
void decode_rmt_(rmt_symbol_word_t *item, size_t item_count);
|
||||
// log the failed RMT call and mark the component failed
|
||||
void fail_(esp_err_t error, const LogString *reason);
|
||||
rmt_channel_handle_t channel_{NULL};
|
||||
uint32_t filter_symbols_{0};
|
||||
uint32_t receive_symbols_{0};
|
||||
bool with_dma_{false};
|
||||
uint32_t carrier_frequency_{0};
|
||||
uint8_t carrier_duty_percent_{100};
|
||||
esp_err_t error_code_{ESP_OK};
|
||||
std::string error_string_;
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_ESP32)
|
||||
|
||||
@@ -43,6 +43,11 @@ static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_r
|
||||
return task_woken != pdFALSE;
|
||||
}
|
||||
|
||||
void RemoteReceiverComponent::fail_(esp_err_t error, const LogString *reason) {
|
||||
ESP_LOGE(TAG, "RMT driver failed: %s", esp_err_to_name(error));
|
||||
this->mark_failed(reason);
|
||||
}
|
||||
|
||||
void RemoteReceiverComponent::setup() {
|
||||
rmt_rx_channel_config_t channel;
|
||||
memset(&channel, 0, sizeof(channel));
|
||||
@@ -55,13 +60,8 @@ void RemoteReceiverComponent::setup() {
|
||||
channel.flags.with_dma = this->with_dma_;
|
||||
esp_err_t error = rmt_new_rx_channel(&channel, &this->channel_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
if (error == ESP_ERR_NOT_FOUND) {
|
||||
this->error_string_ = "out of RMT symbol memory";
|
||||
} else {
|
||||
this->error_string_ = "in rmt_new_rx_channel";
|
||||
}
|
||||
this->mark_failed();
|
||||
this->fail_(error,
|
||||
error == ESP_ERR_NOT_FOUND ? LOG_STR("out of RMT symbol memory") : LOG_STR("in rmt_new_rx_channel"));
|
||||
return;
|
||||
}
|
||||
if (this->pin_->get_flags() & gpio::FLAG_PULLUP) {
|
||||
@@ -71,9 +71,7 @@ void RemoteReceiverComponent::setup() {
|
||||
}
|
||||
error = rmt_enable(this->channel_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_enable";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_enable"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -85,9 +83,7 @@ void RemoteReceiverComponent::setup() {
|
||||
carrier.flags.polarity_active_low = this->pin_->is_inverted();
|
||||
error = rmt_apply_carrier(this->channel_, &carrier);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_apply_carrier";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_apply_carrier"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -97,9 +93,7 @@ void RemoteReceiverComponent::setup() {
|
||||
callbacks.on_recv_done = rmt_callback;
|
||||
error = rmt_rx_register_event_callbacks(this->channel_, &callbacks, &this->store_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_rx_register_event_callbacks";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_rx_register_event_callbacks"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,9 +116,7 @@ void RemoteReceiverComponent::setup() {
|
||||
error = rmt_receive(this->channel_, (uint8_t *) this->store_.buffer + event_size, this->store_.receive_size,
|
||||
&this->store_.config);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_receive";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_receive"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -148,18 +140,11 @@ void RemoteReceiverComponent::dump_config() {
|
||||
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"),
|
||||
this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_),
|
||||
this->error_string_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteReceiverComponent::loop() {
|
||||
if (this->store_.error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Receive error");
|
||||
this->error_code_ = this->store_.error;
|
||||
this->error_string_ = "in rmt_callback";
|
||||
this->mark_failed();
|
||||
this->fail_(this->store_.error, LOG_STR("in rmt_callback"));
|
||||
}
|
||||
if (this->store_.overflow) {
|
||||
ESP_LOGW(TAG, "Buffer overflow");
|
||||
|
||||
@@ -141,6 +141,8 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||
// log the failed RMT call and mark the component failed
|
||||
void fail_(esp_err_t error, const LogString *reason);
|
||||
void configure_rmt_();
|
||||
void wait_for_rmt_();
|
||||
|
||||
@@ -156,8 +158,6 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
|
||||
bool eot_level_{false};
|
||||
rmt_channel_handle_t channel_{NULL};
|
||||
rmt_encoder_handle_t encoder_{NULL};
|
||||
esp_err_t error_code_{ESP_OK};
|
||||
std::string error_string_;
|
||||
bool inverted_{false};
|
||||
bool non_blocking_{false};
|
||||
#endif
|
||||
|
||||
@@ -51,6 +51,11 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
|
||||
}
|
||||
#endif
|
||||
|
||||
void RemoteTransmitterComponent::fail_(esp_err_t error, const LogString *reason) {
|
||||
ESP_LOGE(TAG, "RMT driver failed: %s", esp_err_to_name(error));
|
||||
this->mark_failed(reason);
|
||||
}
|
||||
|
||||
void RemoteTransmitterComponent::setup() {
|
||||
this->inverted_ = this->pin_->is_inverted();
|
||||
this->configure_rmt_();
|
||||
@@ -67,11 +72,6 @@ void RemoteTransmitterComponent::dump_config() {
|
||||
if (this->current_carrier_frequency_ != 0 && this->carrier_duty_percent_ != 100) {
|
||||
ESP_LOGCONFIG(TAG, " Carrier Duty: %u%%", this->carrier_duty_percent_);
|
||||
}
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_),
|
||||
this->error_string_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteTransmitterComponent::digital_write(bool value) {
|
||||
@@ -129,13 +129,8 @@ void RemoteTransmitterComponent::configure_rmt_() {
|
||||
#endif
|
||||
error = rmt_new_tx_channel(&channel, &this->channel_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
if (error == ESP_ERR_NOT_FOUND) {
|
||||
this->error_string_ = "out of RMT symbol memory";
|
||||
} else {
|
||||
this->error_string_ = "in rmt_new_tx_channel";
|
||||
}
|
||||
this->mark_failed();
|
||||
this->fail_(error,
|
||||
error == ESP_ERR_NOT_FOUND ? LOG_STR("out of RMT symbol memory") : LOG_STR("in rmt_new_tx_channel"));
|
||||
return;
|
||||
}
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
@@ -159,9 +154,7 @@ void RemoteTransmitterComponent::configure_rmt_() {
|
||||
encoder.min_chunk_size = 1;
|
||||
error = rmt_new_simple_encoder(&encoder, &this->encoder_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_new_simple_encoder";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_new_simple_encoder"));
|
||||
return;
|
||||
}
|
||||
#else
|
||||
@@ -169,18 +162,14 @@ void RemoteTransmitterComponent::configure_rmt_() {
|
||||
memset(&encoder, 0, sizeof(encoder));
|
||||
error = rmt_new_copy_encoder(&encoder, &this->encoder_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_new_copy_encoder";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_new_copy_encoder"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
error = rmt_enable(this->channel_);
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_enable";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_enable"));
|
||||
return;
|
||||
}
|
||||
this->digital_write(open_drain || this->inverted_);
|
||||
@@ -199,9 +188,7 @@ void RemoteTransmitterComponent::configure_rmt_() {
|
||||
error = rmt_apply_carrier(this->channel_, &carrier);
|
||||
}
|
||||
if (error != ESP_OK) {
|
||||
this->error_code_ = error;
|
||||
this->error_string_ = "in rmt_apply_carrier";
|
||||
this->mark_failed();
|
||||
this->fail_(error, LOG_STR("in rmt_apply_carrier"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,20 +197,20 @@ def _parse_platform_version(value: Any) -> str:
|
||||
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/earlephilhower/arduino-pico/releases
|
||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(6, 0, 0)
|
||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(6, 1, 0)
|
||||
|
||||
# The raspberrypi platform version to use for arduino frameworks
|
||||
# - https://github.com/maxgerhardt/platform-raspberrypi/tags
|
||||
# develop-branch commit carrying the arduino-pico 6.0.0 / pico-quick-toolchain
|
||||
# 5.0.0 (GCC 16.1) update; replace with a release tag when one is cut
|
||||
RECOMMENDED_ARDUINO_PLATFORM_VERSION = "9c167c6b8aac4f4cfa6d55a0c4e5b848795150c0"
|
||||
# develop-branch commit carrying the arduino-pico 6.1.0 update and the board
|
||||
# JSON files it adds; replace with a release tag when one is cut
|
||||
RECOMMENDED_ARDUINO_PLATFORM_VERSION = "5d4561a05e3b212660ac6fdd3fbfb328d1988aa1"
|
||||
|
||||
|
||||
def _arduino_check_versions(value: ConfigType) -> ConfigType:
|
||||
value = value.copy()
|
||||
lookups = {
|
||||
"dev": (cv.Version(6, 0, 0), "https://github.com/earlephilhower/arduino-pico"),
|
||||
"latest": (cv.Version(6, 0, 0), None),
|
||||
"dev": (cv.Version(6, 1, 0), "https://github.com/earlephilhower/arduino-pico"),
|
||||
"latest": (cv.Version(6, 1, 0), None),
|
||||
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
|
||||
}
|
||||
|
||||
|
||||
@@ -1135,6 +1135,18 @@ RP2_BOARD_PINS = {
|
||||
"SS": 5,
|
||||
"TX": 0,
|
||||
},
|
||||
"soldered_nula_node_rp2040": {
|
||||
"MISO": 16,
|
||||
"MOSI": 19,
|
||||
"RX": 1,
|
||||
"SCK": 18,
|
||||
"SCL": 9,
|
||||
"SCL1": 11,
|
||||
"SDA": 8,
|
||||
"SDA1": 10,
|
||||
"SS": 17,
|
||||
"TX": 0,
|
||||
},
|
||||
"soldered_nula_rp2350": {
|
||||
"MISO": 2,
|
||||
"MOSI": 3,
|
||||
@@ -2127,6 +2139,12 @@ BOARDS = {
|
||||
"mcu": "rp2040",
|
||||
"max_pin": 29,
|
||||
},
|
||||
"soldered_nula_node_rp2040": {
|
||||
"name": "Soldered Electronics NULA Node",
|
||||
"mcu": "rp2040",
|
||||
"max_pin": 29,
|
||||
"wifi": True,
|
||||
},
|
||||
"soldered_nula_rp2350": {
|
||||
"name": "Soldered Electronics NULA RP2350",
|
||||
"mcu": "rp2350",
|
||||
|
||||
@@ -55,8 +55,7 @@ namespace esphome::rp2 {
|
||||
|
||||
static const char *const TAG = "rp2.crash";
|
||||
|
||||
// Placed in .noinit so BSS zero-init cannot race with crash_handler_read_and_clear().
|
||||
// The valid field is explicitly cleared in crash_handler_read_and_clear() instead.
|
||||
// Filled from the watchdog scratch registers on the first read.
|
||||
static struct CrashData {
|
||||
bool valid;
|
||||
uint32_t pc;
|
||||
@@ -64,11 +63,24 @@ static struct CrashData {
|
||||
uint32_t sp;
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint8_t backtrace_count;
|
||||
} s_crash_data __attribute__((section(".noinit"))); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
} s_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool crash_handler_has_data() { return s_crash_data.valid; }
|
||||
// Logger::pre_setup() logs the record before App.pre_setup() reaches
|
||||
// arch_init(), so the first caller reads it and later calls are no-ops.
|
||||
// The read clears the scratch registers, so it must not run twice, and
|
||||
// arch_init() keeps its call so the read precedes watchdog_enable(), which
|
||||
// overwrites scratch[4].
|
||||
static bool s_crash_data_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool crash_handler_has_data() {
|
||||
crash_handler_read_and_clear();
|
||||
return s_crash_data.valid;
|
||||
}
|
||||
|
||||
void crash_handler_read_and_clear() {
|
||||
if (s_crash_data_read)
|
||||
return;
|
||||
s_crash_data_read = true;
|
||||
s_crash_data.valid = false;
|
||||
uint32_t magic = watchdog_hw->scratch[0];
|
||||
if ((magic & 0xFFFF0000) == CRASH_MAGIC_SENTINEL && (magic & 0xFFFF) == CRASH_DATA_VERSION) {
|
||||
@@ -97,7 +109,7 @@ void crash_handler_read_and_clear() {
|
||||
// the device crashes again during boot, and allowing the CLI's process_stacktrace
|
||||
// to match and decode each address individually.
|
||||
void crash_handler_log() {
|
||||
if (!s_crash_data.valid)
|
||||
if (!crash_handler_has_data())
|
||||
return;
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
namespace esphome::rp2 {
|
||||
|
||||
/// Read crash data from watchdog scratch registers and clear them.
|
||||
/// Only the first call reads; later calls are no-ops.
|
||||
void crash_handler_read_and_clear();
|
||||
|
||||
/// Log crash data if a crash was detected on previous boot.
|
||||
void crash_handler_log();
|
||||
|
||||
/// Returns true if crash data was found this boot.
|
||||
/// Returns true if crash data was found this boot, reading it first if needed.
|
||||
bool crash_handler_has_data();
|
||||
|
||||
} // namespace esphome::rp2
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
namespace esphome::rp2040_ble {
|
||||
namespace {
|
||||
|
||||
// Pinned against arduino-pico 6.0.0's prebuilt archives: a framework bump (or
|
||||
// Pinned against arduino-pico 6.1.0's prebuilt archives: a framework bump (or
|
||||
// a changed ENABLE_* macro) shifting the struct layout must fail the build
|
||||
// here, not overrun the pool blocks at runtime. Sizes differ per core
|
||||
// architecture (measured from each archive's own storage symbols). GCC only:
|
||||
|
||||
@@ -40,10 +40,7 @@ const LogString *state_class_to_string(StateClass state_class) {
|
||||
return StateClassStrings::get_log_str(static_cast<uint8_t>(state_class), 0);
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
Sensor::Sensor() : state(NAN), raw_state(NAN) {}
|
||||
#pragma GCC diagnostic pop
|
||||
Sensor::Sensor() : state(NAN) {}
|
||||
|
||||
int8_t Sensor::get_accuracy_decimals() {
|
||||
if (this->sensor_flags_.has_accuracy_override)
|
||||
@@ -66,11 +63,8 @@ StateClass Sensor::get_state_class() {
|
||||
}
|
||||
|
||||
void Sensor::publish_state(float state) {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
this->raw_state = state;
|
||||
#pragma GCC diagnostic pop
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
this->raw_state_ = state;
|
||||
this->raw_callback_.call(state);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -96,18 +96,20 @@ class Sensor : public EntityBase {
|
||||
|
||||
/// Getter-syntax for .state.
|
||||
float get_state() const { return this->state; }
|
||||
/// Getter-syntax for .raw_state
|
||||
/// Get the last state received by publish_state(), before any filters were applied.
|
||||
float get_raw_state() const {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return this->raw_state;
|
||||
#pragma GCC diagnostic pop
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
return this->raw_state_;
|
||||
#else
|
||||
return this->state; // No filters compiled in, raw == filtered
|
||||
#endif
|
||||
}
|
||||
|
||||
/** Publish a new state to the front-end.
|
||||
*
|
||||
* First, the new state will be assigned to the raw_value. Then it's passed through all filters
|
||||
* until it finally lands in the .value member variable and a callback is issued.
|
||||
* The value is passed through the filter chain (when filters are compiled in) before landing in
|
||||
* the `state` member and triggering the state callback. The pre-filter value is available via
|
||||
* get_raw_state().
|
||||
*
|
||||
* @param state The state as a floating point number.
|
||||
*/
|
||||
@@ -137,17 +139,11 @@ class Sensor : public EntityBase {
|
||||
*/
|
||||
float state;
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
/// @deprecated Use get_raw_state() instead. This member will be removed in ESPHome 2026.10.0.
|
||||
ESPDEPRECATED("Use get_raw_state() instead of .raw_state. Will be removed in 2026.10.0", "2026.4.0")
|
||||
float raw_state;
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
void internal_send_state_to_frontend(float state);
|
||||
|
||||
protected:
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
float raw_state_{NAN}; ///< The last state passed to publish_state(), before filters.
|
||||
LazyCallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
|
||||
#endif
|
||||
LazyCallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
@@ -66,9 +66,12 @@ static const char *const TAG = "web_server";
|
||||
// GET /{domain}/{device_name}/{entity_name} - sub-device state (USE_DEVICES only)
|
||||
// POST /{domain}/{device_name}/{entity_name}/{action} - sub-device action (USE_DEVICES only)
|
||||
static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, bool is_post = false) {
|
||||
// Every path returns this one object so it is built in place; fields are only set once the URL is known valid
|
||||
UrlMatch match{};
|
||||
|
||||
// URL must start with '/' and have content after it
|
||||
if (url_len < 2 || url_ptr[0] != '/')
|
||||
return UrlMatch{};
|
||||
return match;
|
||||
|
||||
const char *p = url_ptr + 1;
|
||||
const char *end = url_ptr + url_len;
|
||||
@@ -90,15 +93,14 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain,
|
||||
|
||||
// Must have domain with trailing slash
|
||||
if (!s2)
|
||||
return UrlMatch{};
|
||||
|
||||
UrlMatch match{};
|
||||
match.domain = make_ref(s1, s2);
|
||||
match.valid = true;
|
||||
|
||||
if (only_domain || s2 >= end)
|
||||
return match;
|
||||
|
||||
if (only_domain || s2 >= end) {
|
||||
match.domain = make_ref(s1, s2);
|
||||
match.valid = true;
|
||||
return match;
|
||||
}
|
||||
|
||||
// Parse remaining segments only when needed
|
||||
const char *s3 = next_segment(s2);
|
||||
const char *s4 = s3 ? next_segment(s3) : nullptr;
|
||||
@@ -109,7 +111,7 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain,
|
||||
|
||||
// Reject empty segments
|
||||
if (seg2.empty() || (s3 && seg3.empty()) || (s4 && seg4.empty()))
|
||||
return UrlMatch{};
|
||||
return match;
|
||||
|
||||
// Interpret based on segment count
|
||||
if (!s3) {
|
||||
@@ -121,28 +123,31 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain,
|
||||
if (is_post) {
|
||||
match.id = seg2;
|
||||
match.method = seg3;
|
||||
return match;
|
||||
}
|
||||
} else {
|
||||
#ifdef USE_DEVICES
|
||||
match.device_name = seg2;
|
||||
match.id = seg3;
|
||||
match.device_name = seg2;
|
||||
match.id = seg3;
|
||||
#else
|
||||
return UrlMatch{}; // 3-segment GET not supported without USE_DEVICES
|
||||
return match; // 3-segment GET not supported without USE_DEVICES
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
// 3 segments after domain: /{domain}/{device}/{entity}/{action}
|
||||
#ifdef USE_DEVICES
|
||||
if (!is_post) {
|
||||
return UrlMatch{}; // 4-segment GET not supported (action requires POST)
|
||||
return match; // 4-segment GET not supported (action requires POST)
|
||||
}
|
||||
match.device_name = seg2;
|
||||
match.id = seg3;
|
||||
match.method = seg4;
|
||||
#else
|
||||
return UrlMatch{}; // Not supported without USE_DEVICES
|
||||
// Not supported without USE_DEVICES
|
||||
return match;
|
||||
#endif
|
||||
}
|
||||
|
||||
match.domain = make_ref(s1, s2);
|
||||
match.valid = true;
|
||||
return match;
|
||||
}
|
||||
|
||||
@@ -336,6 +341,9 @@ void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSou
|
||||
|
||||
WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {}
|
||||
|
||||
// Kept out of the callers so the 64 bit division is emitted once
|
||||
__attribute__((noinline)) static uint32_t uptime_seconds() { return static_cast<uint32_t>(millis_64() / 1000); }
|
||||
|
||||
json::SerializationBuffer<> WebServer::get_config_json() {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
@@ -343,7 +351,7 @@ json::SerializationBuffer<> WebServer::get_config_json() {
|
||||
root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str();
|
||||
char comment_buffer[Application::ESPHOME_COMMENT_SIZE_MAX];
|
||||
App.get_comment_string(comment_buffer);
|
||||
root[ESPHOME_F("comment")] = comment_buffer;
|
||||
root[ESPHOME_F("comment")] = static_cast<const char *>(comment_buffer);
|
||||
#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
|
||||
root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
|
||||
#else
|
||||
@@ -351,7 +359,7 @@ json::SerializationBuffer<> WebServer::get_config_json() {
|
||||
#endif
|
||||
root[ESPHOME_F("log")] = this->expose_log_;
|
||||
root[ESPHOME_F("lang")] = "en";
|
||||
root[ESPHOME_F("uptime")] = static_cast<uint32_t>(millis_64() / 1000);
|
||||
root[ESPHOME_F("uptime")] = uptime_seconds();
|
||||
|
||||
return builder.serialize();
|
||||
}
|
||||
@@ -382,7 +390,7 @@ void WebServer::setup() {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
char buf[32];
|
||||
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
|
||||
auto uptime = uptime_seconds();
|
||||
size_t len = buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
|
||||
this->events_.try_send_nodefer(buf, len, "ping", millis(), 30000);
|
||||
});
|
||||
@@ -467,7 +475,10 @@ bool WebServer::is_request_origin_allowed_(AsyncWebServerRequest *request, const
|
||||
const size_t scheme_sep = origin.find("://");
|
||||
if (scheme_sep != std::string::npos) {
|
||||
const std::string host = get_request_header(request, "Host");
|
||||
if (!host.empty() && origin.compare(scheme_sep + 3, std::string::npos, host) == 0)
|
||||
// Compare by hand: compare(pos, ...) carries an out_of_range throw path that can never fire here
|
||||
const size_t authority = scheme_sep + 3;
|
||||
if (!host.empty() && origin.size() - authority == host.size() &&
|
||||
memcmp(origin.data() + authority, host.data(), host.size()) == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -534,7 +545,7 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
|
||||
// Helper functions to reduce code size by avoiding macro expansion
|
||||
// Build unique id as: {domain}/{device_name}/{entity_name} or {domain}/{entity_name}
|
||||
// Uses names (not object_id) to avoid UTF-8 collision issues
|
||||
static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
|
||||
static void set_json_id(JsonObject root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
|
||||
const StringRef &name = obj->get_name();
|
||||
size_t prefix_len = strlen(prefix);
|
||||
size_t name_len = name.size();
|
||||
@@ -569,7 +580,7 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
|
||||
#endif
|
||||
memcpy(p, name.c_str(), name_len);
|
||||
p[name_len] = '\0';
|
||||
root[ESPHOME_F("id")] = id_buf;
|
||||
root[ESPHOME_F("id")] = static_cast<const char *>(id_buf);
|
||||
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root[ESPHOME_F("domain")] = prefix;
|
||||
@@ -594,14 +605,13 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
|
||||
// Keep as separate function even though only used once: reduces code size by ~48 bytes
|
||||
// by allowing compiler to share code between template instantiations (bool, float, etc.)
|
||||
template<typename T>
|
||||
static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value,
|
||||
JsonDetail start_config) {
|
||||
static void set_json_value(JsonObject root, EntityBase *obj, const char *prefix, T value, JsonDetail start_config) {
|
||||
set_json_id(root, obj, prefix, start_config);
|
||||
root[ESPHOME_F("value")] = value;
|
||||
}
|
||||
|
||||
template<typename S, typename T>
|
||||
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, S state, const T &value,
|
||||
static void set_json_icon_state_value(JsonObject root, EntityBase *obj, const char *prefix, S state, T value,
|
||||
JsonDetail start_config) {
|
||||
set_json_value(root, obj, prefix, value, start_config);
|
||||
root[ESPHOME_F("state")] = state;
|
||||
@@ -1230,7 +1240,7 @@ json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, Jso
|
||||
// Format: YYYY-MM-DD (max 10 chars + null)
|
||||
char value[12];
|
||||
buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d", obj->year, obj->month, obj->day);
|
||||
set_json_icon_state_value(root, obj, "date", value, value, start_config);
|
||||
set_json_icon_state_value<const char *, const char *>(root, obj, "date", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
@@ -1290,7 +1300,7 @@ json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, Jso
|
||||
// Format: HH:MM:SS (8 chars + null)
|
||||
char value[12];
|
||||
buf_append_printf(value, sizeof(value), 0, "%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
|
||||
set_json_icon_state_value(root, obj, "time", value, value, start_config);
|
||||
set_json_icon_state_value<const char *, const char *>(root, obj, "time", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
@@ -1351,7 +1361,7 @@ json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *
|
||||
char value[24];
|
||||
buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour,
|
||||
obj->minute, obj->second);
|
||||
set_json_icon_state_value(root, obj, "datetime", value, value, start_config);
|
||||
set_json_icon_state_value<const char *, const char *>(root, obj, "datetime", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
@@ -2295,7 +2305,7 @@ json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, J
|
||||
JsonObject root = builder.root();
|
||||
|
||||
set_json_icon_state_value(root, obj, "update", json_state_str(update::update_state_to_string(obj->state)),
|
||||
obj->update_info.latest_version, start_config);
|
||||
obj->update_info.latest_version.c_str(), start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root[ESPHOME_F("current_version")] = obj->update_info.current_version;
|
||||
root[ESPHOME_F("title")] = obj->update_info.title;
|
||||
|
||||
@@ -593,7 +593,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
|
||||
|
||||
web_server_base::WebServerBase *base_;
|
||||
#ifdef USE_ESP32
|
||||
AsyncEventSource events_{"/events", this};
|
||||
AsyncEventSource events_{StringRef::from_lit("/events"), this};
|
||||
#elif USE_ARDUINO
|
||||
DeferredUpdateEventSourceList events_;
|
||||
#endif
|
||||
|
||||
@@ -322,7 +322,7 @@ class AsyncEventSource : public AsyncWebHandler {
|
||||
using connect_handler_t = std::function<void(AsyncEventSourceClient *)>;
|
||||
|
||||
public:
|
||||
AsyncEventSource(std::string url, esphome::web_server::WebServer *ws) : url_(std::move(url)), web_server_(ws) {}
|
||||
AsyncEventSource(StringRef url, esphome::web_server::WebServer *ws) : url_(url), web_server_(ws) {}
|
||||
~AsyncEventSource() override;
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
@@ -352,7 +352,7 @@ class AsyncEventSource : public AsyncWebHandler {
|
||||
// Cold path: move sessions from pending_sessions_ into sessions_ and greet each one.
|
||||
void __attribute__((noinline, cold)) adopt_pending_sessions_main_loop_();
|
||||
|
||||
std::string url_;
|
||||
StringRef url_; // Must outlive this object (string literal)
|
||||
// Main-loop only. Vector: SSE sessions are 1-5 connections, linear search beats set.
|
||||
std::vector<AsyncEventSourceResponse *> sessions_;
|
||||
// Httpd-task intake; guarded by pending_mutex_, gated by has_pending_sessions_.
|
||||
|
||||
@@ -11,6 +11,19 @@
|
||||
#include <esp_chip_info.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_bootloader_desc.h>
|
||||
#include <esp_flash.h>
|
||||
#if __has_include(<esp_flash_chips/spi_flash_chip_generic.h>)
|
||||
#include <esp_flash_chips/spi_flash_chip_generic.h> // ESP-IDF 6
|
||||
#include <esp_flash_chips/spi_flash_chip_mxic.h>
|
||||
#else
|
||||
#include <spi_flash_chip_generic.h>
|
||||
#include <spi_flash_chip_mxic.h>
|
||||
#endif
|
||||
// Vendor flash drivers linked next to the generic one; sdkconfig defines each as 1 or not at all
|
||||
#define ESPHOME_FLASH_VENDOR_DRIVERS \
|
||||
(CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP + CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP + CONFIG_SPI_FLASH_SUPPORT_GD_CHIP + \
|
||||
CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP + CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP + CONFIG_SPI_FLASH_SUPPORT_TH_CHIP + \
|
||||
CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP)
|
||||
#endif
|
||||
#include "esphome/core/version.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -157,8 +170,25 @@ void Application::process_dump_config_() {
|
||||
esp_chip_info(&chip_info);
|
||||
ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100,
|
||||
chip_info.revision % 100, chip_info.cores);
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) && (!defined(USE_ESP32_MIN_CHIP_REVISION_SET) || !defined(USE_ESP32_SRAM1_AS_IRAM))
|
||||
static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced";
|
||||
[[maybe_unused]] static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced";
|
||||
#if ESPHOME_FLASH_VENDOR_DRIVERS > 0
|
||||
{
|
||||
// Only the driver in use earns its IRAM; with several linked at least one is idle
|
||||
const spi_flash_chip_t *flash_driver = esp_flash_default_chip->chip_drv;
|
||||
#if ESPHOME_FLASH_VENDOR_DRIVERS > 1
|
||||
constexpr bool idle_driver = true;
|
||||
#else
|
||||
const bool idle_driver = flash_driver == &esp_flash_chip_generic;
|
||||
#endif
|
||||
if (idle_driver) {
|
||||
const char *value = flash_driver->name;
|
||||
#ifdef CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP
|
||||
if (flash_driver == &esp_flash_chip_mxic_opi)
|
||||
value = "mxic_opi";
|
||||
#endif
|
||||
ESP_LOGW(TAG, "Set flash_chip: %s %s to save IRAM", value, ESP32_ADVANCED_PATH);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET)
|
||||
{
|
||||
|
||||
@@ -67,7 +67,7 @@ static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for qu
|
||||
class Application {
|
||||
public:
|
||||
#ifdef ESPHOME_NAME_ADD_MAC_SUFFIX
|
||||
// Called before Logger::pre_setup() — must not log (global_logger is not yet set).
|
||||
// Runs after Logger::pre_setup() (emitted at EARLY_INIT priority), so the app name is not set yet there.
|
||||
/// Pre-setup with MAC suffix: overwrites placeholder in mutable static buffers with actual MAC.
|
||||
void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len) {
|
||||
arch_init();
|
||||
@@ -87,7 +87,7 @@ class Application {
|
||||
this->friendly_name_ = StringRef(friendly_name, friendly_name_len);
|
||||
}
|
||||
#else
|
||||
// Called before Logger::pre_setup() — must not log (global_logger is not yet set).
|
||||
// Runs after Logger::pre_setup() (emitted at EARLY_INIT priority), so the app name is not set yet there.
|
||||
/// Pre-setup without MAC suffix: StringRef points directly at const string literals in flash.
|
||||
void pre_setup(const char *name, size_t name_len, const char *friendly_name, size_t friendly_name_len) {
|
||||
arch_init();
|
||||
|
||||
+5
-10
@@ -6,18 +6,13 @@ namespace esphome {
|
||||
constinit const Color Color::BLACK(0, 0, 0, 0);
|
||||
constinit const Color Color::WHITE(255, 255, 255, 255);
|
||||
|
||||
Color Color::gradient(const Color &to_color, uint8_t amnt) {
|
||||
uint8_t inv = 255 - amnt;
|
||||
Color new_color;
|
||||
new_color.r = (uint16_t(this->r) * inv + uint16_t(to_color.r) * amnt) / 255;
|
||||
new_color.g = (uint16_t(this->g) * inv + uint16_t(to_color.g) * amnt) / 255;
|
||||
new_color.b = (uint16_t(this->b) * inv + uint16_t(to_color.b) * amnt) / 255;
|
||||
new_color.w = (uint16_t(this->w) * inv + uint16_t(to_color.w) * amnt) / 255;
|
||||
return new_color;
|
||||
Color Color::gradient(const Color &to_color, uint8_t amnt) const {
|
||||
return Color(blend_channel(this->r, to_color.r, amnt), blend_channel(this->g, to_color.g, amnt),
|
||||
blend_channel(this->b, to_color.b, amnt), blend_channel(this->w, to_color.w, amnt));
|
||||
}
|
||||
|
||||
Color Color::fade_to_white(uint8_t amnt) { return this->gradient(Color::WHITE, amnt); }
|
||||
Color Color::fade_to_white(uint8_t amnt) const { return this->gradient(Color::WHITE, amnt); }
|
||||
|
||||
Color Color::fade_to_black(uint8_t amnt) { return this->gradient(Color::BLACK, amnt); }
|
||||
Color Color::fade_to_black(uint8_t amnt) const { return this->gradient(Color::BLACK, amnt); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -174,9 +174,15 @@ struct Color {
|
||||
uint8_t((uint16_t(b) * 255U / max_rgb)), w);
|
||||
}
|
||||
|
||||
Color gradient(const Color &to_color, uint8_t amnt);
|
||||
Color fade_to_white(uint8_t amnt);
|
||||
Color fade_to_black(uint8_t amnt);
|
||||
/// One channel of gradient(): from at amnt 0 to to at amnt 255. Inline so a
|
||||
/// per pixel loop can blend without a call; gradient() itself stays out of
|
||||
/// line so the light effects and fade_to_*() share one copy.
|
||||
static inline uint8_t blend_channel(uint8_t from, uint8_t to, uint8_t amnt) ESPHOME_ALWAYS_INLINE {
|
||||
return (uint16_t(from) * (255 - amnt) + uint16_t(to) * amnt) / 255;
|
||||
}
|
||||
Color gradient(const Color &to_color, uint8_t amnt) const;
|
||||
Color fade_to_white(uint8_t amnt) const;
|
||||
Color fade_to_black(uint8_t amnt) const;
|
||||
|
||||
Color lighten(uint8_t delta) { return *this + delta; }
|
||||
Color darken(uint8_t delta) { return *this - delta; }
|
||||
|
||||
@@ -538,7 +538,7 @@
|
||||
// rp2/__init__.py codegen also defines USE_RP2040 as a back-compat alias
|
||||
// for external custom components that may still test for it.
|
||||
#ifdef USE_RP2
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(6, 0, 0)
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(6, 1, 0)
|
||||
#define USE_RP2_CRASH_HANDLER
|
||||
#define USE_HTTP_REQUEST_RESPONSE
|
||||
#define USE_I2C
|
||||
|
||||
@@ -9,16 +9,19 @@ byte-identical to PlatformIO's output:
|
||||
Flash: [=== ] 48.4% (used 888511 bytes from 1835008 bytes)
|
||||
|
||||
The format matches ``script/ci_memory_impact_extract.py`` so CI memory
|
||||
analysis works unchanged on native ESP-IDF builds. RAM total is the
|
||||
DRAM region size from the linker map; Flash total is taken from
|
||||
analysis works unchanged on native ESP-IDF builds. RAM usage comes from
|
||||
the DRAM (or unified DIRAM) region of the linker map. Flash used is the
|
||||
exact image size matching the ``Total image size`` line: json2
|
||||
``total_size`` when present, otherwise derived from the ELF (see
|
||||
``_image_size_from_elf``). Flash total is taken from
|
||||
``partitions.csv`` using PlatformIO's rule (first app partition whose
|
||||
subtype is ``factory`` or ``ota_0``; see
|
||||
``platform-espressif32/builder/main.py::_update_max_upload_size``).
|
||||
|
||||
Structured size data is produced at link time by a CMake POST_BUILD
|
||||
custom command (see ``build_gen/espidf.py``) which writes
|
||||
``esp_idf_size.json`` next to the ELF. We read that file here rather
|
||||
than re-running ``esp_idf_size`` from Python.
|
||||
``esp_idf_size.json`` (``--format=json2``, a per-memory-type summary)
|
||||
next to the ELF; we read that rather than re-running ``esp_idf_size``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,6 +30,7 @@ import csv
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import struct
|
||||
|
||||
from esphome.build_helpers.size_summary import print_size_line
|
||||
|
||||
@@ -69,11 +73,43 @@ def _find_app_partition_size(partitions_csv: Path) -> int:
|
||||
raise ValueError(f"No app+factory or app+ota_0 partition in {partitions_csv}")
|
||||
|
||||
|
||||
def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
|
||||
def _image_size_from_elf(elf: Path) -> int:
|
||||
"""Sum the allocated PROGBITS section sizes from an ELF32 file.
|
||||
|
||||
Matches ``esp_idf_size.ng.memorymap._get_image_size`` byte for byte;
|
||||
esptool's ``ELFFile`` filters sections differently and would not.
|
||||
Raises ``ValueError`` for anything but a well-formed ELF32 LE file.
|
||||
"""
|
||||
with elf.open("rb") as f:
|
||||
header = f.read(52) # ELF32 header
|
||||
if len(header) < 52 or header[:6] != b"\x7fELF\x01\x01":
|
||||
raise ValueError(f"{elf} is not a 32-bit little-endian ELF")
|
||||
(e_shoff,) = struct.unpack_from("<I", header, 0x20) # e_shoff
|
||||
e_shentsize, e_shnum = struct.unpack_from("<HH", header, 0x2E)
|
||||
if e_shentsize < 40: # sizeof(Elf32_Shdr)
|
||||
raise ValueError(f"{elf} has an invalid section header size")
|
||||
f.seek(e_shoff)
|
||||
table = f.read(e_shnum * e_shentsize)
|
||||
if len(table) < e_shnum * e_shentsize:
|
||||
raise ValueError(f"{elf} has a truncated section header table")
|
||||
total = 0
|
||||
for off in range(0, e_shnum * e_shentsize, e_shentsize):
|
||||
sh_type, sh_flags = struct.unpack_from("<II", table, off + 4)
|
||||
(sh_size,) = struct.unpack_from("<I", table, off + 20)
|
||||
if sh_type == 1 and sh_flags & 0x2: # SHT_PROGBITS with SHF_ALLOC
|
||||
total += sh_size
|
||||
if total == 0:
|
||||
# A used-0-bytes Flash line would read as a real measurement
|
||||
raise ValueError(f"{elf} has no allocated PROGBITS sections")
|
||||
return total
|
||||
|
||||
|
||||
def print_summary(size_json: Path, partitions_csv: Path, firmware_elf: Path) -> None:
|
||||
"""Print PlatformIO-shaped RAM and Flash one-liners.
|
||||
|
||||
Failures are non-fatal: the build has already succeeded, we just couldn't
|
||||
summarize. Logs the cause at debug level.
|
||||
summarize. Anomalies (missing region, unreadable ELF) warn; expected
|
||||
optional inputs (no size json, no partitions.csv) log at debug.
|
||||
"""
|
||||
if not size_json.is_file():
|
||||
_LOGGER.debug("Skipping size summary: %s not found", size_json)
|
||||
@@ -83,20 +119,49 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
_LOGGER.debug("Skipping size summary: %s", e)
|
||||
return
|
||||
|
||||
memory_types = data.get("memory_types", {})
|
||||
ram_region = memory_types.get("DRAM") or memory_types.get("DIRAM") or {}
|
||||
ram_used = ram_region.get("used")
|
||||
ram_total = ram_region.get("size")
|
||||
if ram_total and ram_used is not None:
|
||||
print_size_line("RAM", ram_used, ram_total)
|
||||
|
||||
image_size = data.get("image_size")
|
||||
if image_size is None or partitions_csv is None:
|
||||
if not isinstance(data, dict):
|
||||
_LOGGER.warning("Skipping size summary: unexpected json shape in %s", size_json)
|
||||
return
|
||||
|
||||
layout = data.get("layout")
|
||||
regions = {
|
||||
entry.get("name"): entry
|
||||
for entry in (layout if isinstance(layout, list) else [])
|
||||
if isinstance(entry, dict)
|
||||
}
|
||||
# Every chip has a DRAM or DIRAM region, so a warning here usually
|
||||
# means the esp_idf_size json schema changed
|
||||
ram_region = regions.get("DRAM") or regions.get("DIRAM")
|
||||
if ram_region is None:
|
||||
_LOGGER.warning("Skipping RAM summary: no DRAM/DIRAM region in %s", size_json)
|
||||
elif (
|
||||
isinstance(ram_total := ram_region.get("total"), int)
|
||||
and ram_total > 0
|
||||
and isinstance(ram_used := ram_region.get("used"), int)
|
||||
):
|
||||
print_size_line("RAM", ram_used, ram_total)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Skipping RAM summary: unusable region %s in %s", ram_region, size_json
|
||||
)
|
||||
|
||||
# esp-idf-size >= 2.1 (IDF >= 6.0) reports the exact image size in
|
||||
# json2; older 1.x omits it, so derive the same figure from the ELF.
|
||||
flash_used = data.get("total_size")
|
||||
if not (isinstance(flash_used, int) and flash_used > 0):
|
||||
_LOGGER.debug("No total_size in %s, deriving from %s", size_json, firmware_elf)
|
||||
try:
|
||||
flash_used = _image_size_from_elf(firmware_elf)
|
||||
except (OSError, ValueError) as e:
|
||||
# The ELF must be present and well formed after a successful build
|
||||
_LOGGER.warning("Skipping Flash summary: %s", e)
|
||||
return
|
||||
try:
|
||||
app_size = _find_app_partition_size(partitions_csv)
|
||||
except ValueError as e:
|
||||
except (OSError, ValueError) as e:
|
||||
_LOGGER.debug("Skipping Flash summary: %s", e)
|
||||
return
|
||||
print_size_line("Flash", image_size, app_size)
|
||||
if app_size <= 0:
|
||||
_LOGGER.debug("Skipping Flash summary: app partition size is 0")
|
||||
return
|
||||
print_size_line("Flash", flash_used, app_size)
|
||||
|
||||
@@ -542,7 +542,7 @@ def run_compile(config, verbose: bool) -> int:
|
||||
if rc == 0:
|
||||
size_json = CORE.relative_build_path("build", "esp_idf_size.json")
|
||||
partitions = CORE.relative_build_path("partitions.csv")
|
||||
print_summary(size_json, partitions if partitions.is_file() else None)
|
||||
print_summary(size_json, partitions, get_built_elf_path())
|
||||
return rc
|
||||
|
||||
|
||||
@@ -579,6 +579,16 @@ def get_ota_firmware_path() -> Path:
|
||||
return build_dir / "firmware.ota.bin"
|
||||
|
||||
|
||||
def get_built_elf_path() -> Path:
|
||||
"""Path to the ELF idf.py writes directly, ``<build>/<name>.elf``.
|
||||
|
||||
Exists as soon as the build finishes, unlike the ``firmware.elf``
|
||||
copy that ``create_elf_copy`` makes later.
|
||||
"""
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
return build_dir / f"{CORE.name}.elf"
|
||||
|
||||
|
||||
def get_elf_path() -> Path:
|
||||
"""Get the path to the firmware ELF file.
|
||||
|
||||
@@ -706,8 +716,7 @@ def create_elf_copy() -> bool:
|
||||
"download ELF" link requests the literal filename ``firmware.elf``
|
||||
(PlatformIO convention), so copy it to that name.
|
||||
"""
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
src_elf = build_dir / f"{CORE.name}.elf"
|
||||
src_elf = get_built_elf_path()
|
||||
dst_elf = get_elf_path()
|
||||
|
||||
if not src_elf.is_file():
|
||||
|
||||
@@ -4,7 +4,7 @@ dependencies:
|
||||
esphome/dlms_parser:
|
||||
version: 1.1.0
|
||||
esphome/esp-audio-libs:
|
||||
version: 4.0.0
|
||||
version: 4.0.1
|
||||
esphome/esp-micro-speech-features:
|
||||
version: 1.2.3
|
||||
esphome/micro-decoder:
|
||||
|
||||
+2
-2
@@ -203,11 +203,11 @@ extra_scripts =
|
||||
extends = common:arduino
|
||||
board_build.filesystem_size = 0.5m
|
||||
|
||||
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#9c167c6b8aac4f4cfa6d55a0c4e5b848795150c0
|
||||
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#5d4561a05e3b212660ac6fdd3fbfb328d1988aa1
|
||||
platform_packages =
|
||||
; The framework-arduinopico package is no longer published to the PlatformIO
|
||||
; registry, so install the framework straight from the GitHub release
|
||||
earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/6.0.0/rp2040-6.0.0.zip
|
||||
earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/6.1.0/rp2040-6.1.0.zip
|
||||
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
flash_chip: gd
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
flash_chip: generic
|
||||
@@ -0,0 +1,10 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
flash_mode: opi
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
flash_chip: mxic_opi
|
||||
@@ -10,6 +10,7 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32 import (
|
||||
ESP32_FLASH_CHIPS,
|
||||
KEY_FATFS_REQUIRED,
|
||||
KEY_MBEDTLS_TLS_EXTRAS_REQUIRED,
|
||||
KEY_MBEDTLS_TLS_SERVER_REQUIRED,
|
||||
@@ -252,6 +253,41 @@ def test_esp32_rejects_unsupported_cli_toolchain(
|
||||
r"value must be at most 5 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]\['key_id'\]",
|
||||
id="nvs_encryption_key_id_out_of_range",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32",
|
||||
"board": "esp32dev",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"flash_chip": "mxic_opi"},
|
||||
},
|
||||
},
|
||||
r"'flash_chip: mxic_opi' is only supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['flash_chip'\]",
|
||||
id="flash_chip_mxic_opi_only_on_s3",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32s3",
|
||||
"flash_mode": "opi",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"flash_chip": "gd"},
|
||||
},
|
||||
},
|
||||
r"'flash_chip: gd' does not match 'flash_mode: opi'; octal flash uses mxic_opi @ data\['framework'\]\['advanced'\]\['flash_chip'\]",
|
||||
id="flash_chip_must_match_opi_mode",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32s3",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"flash_chip": "mxic_opi"},
|
||||
},
|
||||
},
|
||||
r"'flash_chip: mxic_opi' requires 'flash_mode: opi' @ data\['framework'\]\['advanced'\]\['flash_chip'\]",
|
||||
id="flash_chip_mxic_opi_requires_opi_mode",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32",
|
||||
@@ -719,6 +755,43 @@ def test_flash_mode_sets_sdkconfig_and_pio_option(
|
||||
assert CORE.platformio_options.get("board_build.f_flash") == "80000000L"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "enabled"),
|
||||
[
|
||||
pytest.param("flash_chip_gd.yaml", "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP", id="gd"),
|
||||
pytest.param("flash_chip_generic.yaml", None, id="generic"),
|
||||
pytest.param(
|
||||
"flash_chip_mxic_opi_s3.yaml",
|
||||
"CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP",
|
||||
id="mxic_opi_s3",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_flash_chip_keeps_one_vendor_driver(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
enabled: str | None,
|
||||
) -> None:
|
||||
"""flash_chip enables only the chosen vendor driver."""
|
||||
generate_main(component_config_path(config_file))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
vendors = {
|
||||
k: v for k, v in sdkconfig.items() if k.startswith("CONFIG_SPI_FLASH_SUPPORT_")
|
||||
}
|
||||
assert vendors == {flag: flag == enabled for flag in ESP32_FLASH_CHIPS.values()}
|
||||
|
||||
|
||||
def test_flash_chip_unset_keeps_idf_defaults(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Without flash_chip every vendor driver stays at its ESP-IDF default."""
|
||||
generate_main(component_config_path("flash_mode_default.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert not any(key.startswith("CONFIG_SPI_FLASH_SUPPORT_") for key in sdkconfig)
|
||||
|
||||
|
||||
def test_flash_mode_opi_enables_octal_flash(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
bk72xx:
|
||||
board: generic-bk7252
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: P6
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-c61-devkitc1
|
||||
variant: esp32c61
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: GPIO4
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
ln882x:
|
||||
board: generic-ln882h
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: PA4
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
rp2:
|
||||
board: rpipicow
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: GPIO4
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
rtl87xx:
|
||||
board: generic-rtl8710bn-2mb-788k
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: PA12
|
||||
@@ -1,8 +1,16 @@
|
||||
"""buffer_size reaches the receiver when set, and always on the pulse ring targets."""
|
||||
"""buffer_size is bytes on the pulse ring targets and only reaches RMT targets when set."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import remote_receiver
|
||||
from esphome.components.esp8266 import gpio as esp8266_gpio # noqa: F401 registers the pin schema
|
||||
from esphome.config_validation import Invalid
|
||||
from esphome.const import PlatformFramework
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def test_explicit_buffer_size_is_passed_through(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
@@ -12,17 +20,29 @@ def test_explicit_buffer_size_is_passed_through(
|
||||
assert "rcvr->set_buffer_size(2000);" in main_cpp
|
||||
|
||||
|
||||
def test_pulse_ring_target_keeps_a_default(
|
||||
@pytest.mark.parametrize(
|
||||
"target", ["esp8266", "rp2", "bk72xx", "rtl87xx", "ln882x", "esp32_c2", "esp32_c61"]
|
||||
)
|
||||
def test_pulse_ring_default_holds_1000_pulses(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
target: str,
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("receiver_esp8266.yaml"))
|
||||
assert "rcvr->set_buffer_size(1000);" in main_cpp
|
||||
main_cpp = generate_main(component_config_path(f"receiver_{target}.yaml"))
|
||||
assert "rcvr->set_buffer_size(4000);" in main_cpp
|
||||
|
||||
|
||||
def test_esp32_variant_without_rmt_keeps_a_default(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[("32b", None), ("64b", 64), ("65b", 65), ("65535b", 65535), ("65536b", None)],
|
||||
)
|
||||
def test_buffer_size_range(
|
||||
set_core_config: SetCoreConfigCallable, value: str, expected: int | None
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("receiver_esp32_c2.yaml"))
|
||||
assert "rcvr->set_buffer_size(1000);" in main_cpp
|
||||
set_core_config(PlatformFramework.ESP8266_ARDUINO)
|
||||
config = {"pin": "GPIO4", "buffer_size": value}
|
||||
if expected is None:
|
||||
with pytest.raises(Invalid):
|
||||
remote_receiver.CONFIG_SCHEMA(config)
|
||||
else:
|
||||
assert remote_receiver.CONFIG_SCHEMA(config)["buffer_size"] == expected
|
||||
|
||||
@@ -22,6 +22,7 @@ esp32:
|
||||
disable_regi2c_in_iram: true
|
||||
disable_fatfs: true
|
||||
sram1_as_iram: true
|
||||
flash_chip: gd
|
||||
watchdog_timeout: 7s
|
||||
|
||||
wifi:
|
||||
|
||||
@@ -9,6 +9,7 @@ esp32:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
execute_from_psram: true
|
||||
flash_chip: gd
|
||||
disable_libc_locks_in_iram: true # Test default RAM optimization enabled
|
||||
disable_debug_stubs: true
|
||||
disable_ocd_aware: true
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
esphome:
|
||||
name: test-sensor-raw-state
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# Filters are compiled in for this config (USE_SENSOR_FILTER), so raw storage exists
|
||||
sensor:
|
||||
# No filters on this sensor: get_raw_state() must equal state
|
||||
- platform: template
|
||||
name: "No Filter Sensor"
|
||||
id: no_filter_sensor
|
||||
accuracy_decimals: 1
|
||||
|
||||
# Filtered sensor: get_raw_state() must be the pre-filter value
|
||||
- platform: template
|
||||
name: "With Filter Sensor"
|
||||
id: with_filter_sensor
|
||||
accuracy_decimals: 1
|
||||
filters:
|
||||
- multiply: 2.0
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test No Filter Button"
|
||||
id: test_no_filter_button
|
||||
on_press:
|
||||
- sensor.template.publish:
|
||||
id: no_filter_sensor
|
||||
state: 21.5
|
||||
- delay: 50ms
|
||||
- logger.log:
|
||||
format: "NO_FILTER: state=%.1f raw_state=%.1f"
|
||||
args:
|
||||
- id(no_filter_sensor).state
|
||||
- id(no_filter_sensor).get_raw_state()
|
||||
|
||||
- platform: template
|
||||
name: "Test With Filter Button"
|
||||
id: test_with_filter_button
|
||||
on_press:
|
||||
- sensor.template.publish:
|
||||
id: with_filter_sensor
|
||||
state: 21.5
|
||||
- delay: 50ms
|
||||
- logger.log:
|
||||
format: "WITH_FILTER: state=%.1f raw_state=%.1f"
|
||||
args:
|
||||
- id(with_filter_sensor).state
|
||||
- id(with_filter_sensor).get_raw_state()
|
||||
@@ -0,0 +1,31 @@
|
||||
esphome:
|
||||
name: test-sensor-raw-state-no-filter
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# No sensor in this config has filters, so USE_SENSOR_FILTER is not defined and
|
||||
# get_raw_state() falls back to state
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "No Filter Sensor"
|
||||
id: no_filter_sensor
|
||||
accuracy_decimals: 1
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test No Filter Button"
|
||||
id: test_no_filter_button
|
||||
on_press:
|
||||
- sensor.template.publish:
|
||||
id: no_filter_sensor
|
||||
state: 21.5
|
||||
- delay: 50ms
|
||||
- logger.log:
|
||||
format: "NO_FILTER: state=%.1f raw_state=%.1f"
|
||||
args:
|
||||
- id(no_filter_sensor).state
|
||||
- id(no_filter_sensor).get_raw_state()
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Integration tests for Sensor::get_raw_state().
|
||||
|
||||
Raw state storage only exists when filters are compiled in (USE_SENSOR_FILTER).
|
||||
Without it, get_raw_state() returns state, so both build configurations are covered:
|
||||
one fixture with a filtered sensor and one with no filters at all.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from aioesphomeapi import APIClient, EntityInfo
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
NO_FILTER_PATTERN = re.compile(r"NO_FILTER: state=([\d.]+) raw_state=([\d.]+)")
|
||||
WITH_FILTER_PATTERN = re.compile(r"WITH_FILTER: state=([\d.]+) raw_state=([\d.]+)")
|
||||
|
||||
|
||||
async def _press_and_read(
|
||||
client: APIClient,
|
||||
entities: list[EntityInfo],
|
||||
button_object_id: str,
|
||||
future: asyncio.Future[tuple[float, float]],
|
||||
label: str,
|
||||
) -> tuple[float, float]:
|
||||
button = next(
|
||||
(e for e in entities if button_object_id in e.object_id.lower()), None
|
||||
)
|
||||
assert button is not None, f"{button_object_id} not found"
|
||||
client.button_command(button.key)
|
||||
try:
|
||||
return await asyncio.wait_for(future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Timeout waiting for {label} log message")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_raw_state(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""With filters compiled in, raw state is stored separately from state."""
|
||||
loop = asyncio.get_running_loop()
|
||||
no_filter_future: asyncio.Future[tuple[float, float]] = loop.create_future()
|
||||
with_filter_future: asyncio.Future[tuple[float, float]] = loop.create_future()
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
if not no_filter_future.done() and (match := NO_FILTER_PATTERN.search(line)):
|
||||
no_filter_future.set_result((float(match.group(1)), float(match.group(2))))
|
||||
if not with_filter_future.done() and (
|
||||
match := WITH_FILTER_PATTERN.search(line)
|
||||
):
|
||||
with_filter_future.set_result(
|
||||
(float(match.group(1)), float(match.group(2)))
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
state, raw_state = await _press_and_read(
|
||||
client, entities, "test_no_filter_button", no_filter_future, "NO_FILTER"
|
||||
)
|
||||
assert state == 21.5
|
||||
assert raw_state == 21.5
|
||||
|
||||
state, raw_state = await _press_and_read(
|
||||
client,
|
||||
entities,
|
||||
"test_with_filter_button",
|
||||
with_filter_future,
|
||||
"WITH_FILTER",
|
||||
)
|
||||
assert state == 43.0
|
||||
assert raw_state == 21.5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_raw_state_no_filter(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Without filters compiled in, get_raw_state() returns state."""
|
||||
loop = asyncio.get_running_loop()
|
||||
no_filter_future: asyncio.Future[tuple[float, float]] = loop.create_future()
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
if not no_filter_future.done() and (match := NO_FILTER_PATTERN.search(line)):
|
||||
no_filter_future.set_result((float(match.group(1)), float(match.group(2))))
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
state, raw_state = await _press_and_read(
|
||||
client, entities, "test_no_filter_button", no_filter_future, "NO_FILTER"
|
||||
)
|
||||
assert state == 21.5
|
||||
assert raw_state == 21.5
|
||||
@@ -163,6 +163,18 @@ def test_has_discovered_components_after_configure(tmp_path: Path) -> None:
|
||||
assert has_discovered_components()
|
||||
|
||||
|
||||
def test_get_project_cmakelists_size_command_uses_json2() -> None:
|
||||
"""The POST_BUILD size command uses the cheap json2 format, with --ng
|
||||
only on the 1.x tool bundled with IDF < 6."""
|
||||
content = _render()
|
||||
assert "-m esp_idf_size --ng --format=json2" in content
|
||||
|
||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = cv.Version(6, 0, 0)
|
||||
content = _render()
|
||||
assert "--ng" not in content
|
||||
assert "--format=json2" in content
|
||||
|
||||
|
||||
def test_get_project_cmakelists_uses_supplied_builtin_components() -> None:
|
||||
"""A cached list replaces project_description.json and is still filtered
|
||||
by EXCLUDE_COMPONENTS."""
|
||||
|
||||
@@ -638,6 +638,43 @@ def test_run_compile_passes_compile_process_limit(setup_core: Path) -> None:
|
||||
mock_run.assert_called_once_with("build", "size", jobs=1)
|
||||
|
||||
|
||||
def test_run_compile_passes_size_summary_paths(setup_core: Path) -> None:
|
||||
"""print_summary receives the size json, partitions.csv, and the built
|
||||
ELF from get_built_elf_path, which must stay in lockstep with the
|
||||
project() name in the generated CMakeLists."""
|
||||
_setup_build(setup_core)
|
||||
config = {CONF_ESPHOME: {}}
|
||||
|
||||
with (
|
||||
patch.object(toolchain, "need_reconfigure", return_value=False),
|
||||
patch.object(toolchain, "run_idf_py", return_value=0),
|
||||
patch.object(toolchain, "print_summary") as mock_summary,
|
||||
):
|
||||
assert toolchain.run_compile(config, verbose=False) == 0
|
||||
|
||||
mock_summary.assert_called_once_with(
|
||||
CORE.relative_build_path("build", "esp_idf_size.json"),
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
CORE.relative_build_path("build", f"{CORE.name}.elf"),
|
||||
)
|
||||
|
||||
|
||||
def test_create_elf_copy(setup_core: Path) -> None:
|
||||
"""The built <name>.elf is copied to the firmware.elf dashboard name."""
|
||||
_setup_build(setup_core)
|
||||
src = toolchain.get_built_elf_path()
|
||||
src.parent.mkdir(parents=True, exist_ok=True)
|
||||
src.write_bytes(b"elf")
|
||||
assert toolchain.create_elf_copy() is True
|
||||
assert toolchain.get_elf_path().read_bytes() == b"elf"
|
||||
|
||||
|
||||
def test_create_elf_copy_missing_source(setup_core: Path) -> None:
|
||||
"""A missing built ELF is a warning and False, not a crash."""
|
||||
_setup_build(setup_core)
|
||||
assert toolchain.create_elf_copy() is False
|
||||
|
||||
|
||||
def test_run_compile_without_compile_process_limit(setup_core: Path) -> None:
|
||||
"""When no compile_process_limit is set, no job limit is passed to idf.py."""
|
||||
_setup_build(setup_core)
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -17,64 +19,106 @@ def _write_size_json(tmp_path: Path, data: dict) -> Path:
|
||||
return out
|
||||
|
||||
|
||||
def _write_partitions(tmp_path: Path) -> Path:
|
||||
"""Drop a partitions.csv with a 0x1C0000 (1835008 byte) app slot."""
|
||||
out = tmp_path / "partitions.csv"
|
||||
out.write_text(
|
||||
"# name, type, subtype, offset, size, flags\n"
|
||||
"app0, app, ota_0, 0x10000, 0x1C0000,\n"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _elf_bytes(sections: list[tuple[int, int, int]], shentsize: int = 40) -> bytes:
|
||||
"""Build a minimal ELF32 LE whose section headers carry the given
|
||||
(sh_type, sh_flags, sh_size) triples."""
|
||||
out = bytearray(52)
|
||||
out[0:4] = b"\x7fELF"
|
||||
out[4] = out[5] = 1 # 32-bit, little-endian
|
||||
struct.pack_into("<I", out, 0x20, 52) # e_shoff
|
||||
struct.pack_into("<HH", out, 0x2E, shentsize, len(sections))
|
||||
for sh_type, sh_flags, sh_size in sections:
|
||||
shdr = bytearray(40)
|
||||
struct.pack_into("<II", shdr, 4, sh_type, sh_flags)
|
||||
struct.pack_into("<I", shdr, 20, sh_size)
|
||||
out += shdr
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def _esp32_size_data() -> dict:
|
||||
"""Synthetic esp_idf_size.json for the original ESP32 (split IRAM/DRAM)."""
|
||||
"""Synthetic json2 for the original ESP32 (split IRAM/DRAM), in the
|
||||
esp-idf-size >= 2.1 shape that carries ``total_size``."""
|
||||
return {
|
||||
"image_size": 827455,
|
||||
"memory_types": {
|
||||
"DRAM": {
|
||||
"size": 180736,
|
||||
"version": "1.1",
|
||||
"total_size": 827455,
|
||||
"layout": [
|
||||
{
|
||||
"name": "DRAM",
|
||||
"total": 180736,
|
||||
"used": 47332,
|
||||
"sections": {
|
||||
".dram0.bss": {"abbrev_name": ".bss", "size": 30616},
|
||||
".dram0.data": {"abbrev_name": ".data", "size": 16716},
|
||||
"free": 133404,
|
||||
"parts": {
|
||||
".bss": {"size": 30616},
|
||||
".data": {"size": 16716},
|
||||
},
|
||||
},
|
||||
"IRAM": {
|
||||
"size": 131072,
|
||||
{
|
||||
"name": "IRAM",
|
||||
"total": 131072,
|
||||
"used": 80351,
|
||||
"sections": {
|
||||
".iram0.text": {"abbrev_name": ".text", "size": 79323},
|
||||
".iram0.vectors": {"abbrev_name": ".vectors", "size": 1028},
|
||||
"free": 50721,
|
||||
"parts": {
|
||||
".text": {"size": 79323},
|
||||
".vectors": {"size": 1028},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _s3_size_data() -> dict:
|
||||
"""Synthetic esp_idf_size.json for ESP32-S3 (unified DIRAM)."""
|
||||
"""Synthetic json2 for ESP32-S3 (unified DIRAM), in the esp-idf-size 1.x
|
||||
shape without ``total_size``."""
|
||||
return {
|
||||
"image_size": 724215,
|
||||
"memory_types": {
|
||||
"DIRAM": {
|
||||
"size": 341760,
|
||||
"version": "1.1",
|
||||
"layout": [
|
||||
{
|
||||
"name": "DIRAM",
|
||||
"total": 341760,
|
||||
"used": 104999,
|
||||
"sections": {
|
||||
".iram0.text": {"abbrev_name": ".text", "size": 58051},
|
||||
".dram0.bss": {"abbrev_name": ".bss", "size": 27088},
|
||||
".dram0.data": {"abbrev_name": ".data", "size": 19708},
|
||||
".noinit": {"abbrev_name": ".noinit", "size": 152},
|
||||
"free": 236761,
|
||||
"parts": {
|
||||
".text": {"size": 58051},
|
||||
".bss": {"size": 27088},
|
||||
".data": {"size": 19708},
|
||||
".noinit": {"size": 152},
|
||||
},
|
||||
},
|
||||
"IRAM": {
|
||||
"size": 16384,
|
||||
{
|
||||
"name": "IRAM",
|
||||
"total": 16384,
|
||||
"used": 16384,
|
||||
"sections": {
|
||||
".iram0.text": {"abbrev_name": ".text", "size": 15356},
|
||||
".iram0.vectors": {"abbrev_name": ".vectors", "size": 1028},
|
||||
"free": 0,
|
||||
"parts": {
|
||||
".text": {"size": 15356},
|
||||
".vectors": {"size": 1028},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _print_summary_ram_only(tmp_path: Path, size_json: Path) -> None:
|
||||
"""Call print_summary with no partitions.csv or ELF on disk."""
|
||||
print_summary(size_json, tmp_path / "partitions.csv", tmp_path / "firmware.elf")
|
||||
|
||||
|
||||
def test_print_summary_esp32_uses_dram(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""Original ESP32: DRAM has no ``.text``, so RAM = DRAM.used / DRAM.size unchanged."""
|
||||
"""Original ESP32: RAM = DRAM.used / DRAM.total."""
|
||||
size_json = _write_size_json(tmp_path, _esp32_size_data())
|
||||
print_summary(size_json, partitions_csv=None)
|
||||
_print_summary_ram_only(tmp_path, size_json)
|
||||
out = capsys.readouterr().out
|
||||
assert "RAM:" in out
|
||||
assert "used 47332 bytes from 180736 bytes" in out
|
||||
@@ -83,63 +127,193 @@ def test_print_summary_esp32_uses_dram(
|
||||
def test_print_summary_s3_falls_back_to_diram(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""ESP32-S3 with no DRAM key falls back to DIRAM and reports raw region usage."""
|
||||
"""ESP32-S3 with no DRAM entry falls back to DIRAM and reports raw region usage."""
|
||||
size_json = _write_size_json(tmp_path, _s3_size_data())
|
||||
print_summary(size_json, partitions_csv=None)
|
||||
_print_summary_ram_only(tmp_path, size_json)
|
||||
out = capsys.readouterr().out
|
||||
assert "used 104999 bytes from 341760 bytes" in out
|
||||
|
||||
|
||||
def test_print_summary_skips_when_diram_total_collapses(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A zero-size region drops the RAM line rather than divide by zero."""
|
||||
size_json = _write_size_json(
|
||||
tmp_path,
|
||||
{
|
||||
"memory_types": {
|
||||
"DIRAM": {
|
||||
"size": 0,
|
||||
"used": 0,
|
||||
"sections": {},
|
||||
},
|
||||
},
|
||||
"version": "1.1",
|
||||
"layout": [{"name": "DIRAM", "total": 0, "used": 0}],
|
||||
},
|
||||
)
|
||||
print_summary(size_json, partitions_csv=None)
|
||||
_print_summary_ram_only(tmp_path, size_json)
|
||||
out = capsys.readouterr().out
|
||||
assert "RAM:" not in out
|
||||
assert "unusable region" in caplog.text
|
||||
|
||||
|
||||
def test_print_summary_handles_missing_json(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""Missing size json is non-fatal and prints nothing."""
|
||||
print_summary(tmp_path / "does_not_exist.json", partitions_csv=None)
|
||||
_print_summary_ram_only(tmp_path, tmp_path / "does_not_exist.json")
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
|
||||
def test_print_summary_handles_no_memory_types(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
def test_print_summary_handles_no_layout(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A size json without ``memory_types`` still doesn't crash."""
|
||||
size_json = _write_size_json(tmp_path, {"image_size": 0})
|
||||
print_summary(size_json, partitions_csv=None)
|
||||
"""A size json without ``layout`` warns so schema drift is visible."""
|
||||
size_json = _write_size_json(tmp_path, {"version": "1.1"})
|
||||
_print_summary_ram_only(tmp_path, size_json)
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
|
||||
def test_print_summary_flash_line(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""A partition table with an app row yields the Flash line in the exact
|
||||
padded shape script/ci_memory_impact_extract.py greps."""
|
||||
size_json = _write_size_json(tmp_path, _esp32_size_data())
|
||||
partitions = tmp_path / "partitions.csv"
|
||||
partitions.write_text(
|
||||
"# name, type, subtype, offset, size, flags\n"
|
||||
"app0, app, ota_0, 0x10000, 0x1C0000,\n"
|
||||
assert any(
|
||||
r.levelname == "WARNING" and "no DRAM/DIRAM region" in r.message
|
||||
for r in caplog.records
|
||||
)
|
||||
print_summary(size_json, partitions)
|
||||
|
||||
|
||||
def test_print_summary_flash_line_prefers_total_size(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""With ``total_size`` in the json, that figure wins without reading the
|
||||
ELF, in the exact shape script/ci_memory_impact_extract.py greps."""
|
||||
size_json = _write_size_json(tmp_path, _esp32_size_data())
|
||||
partitions = _write_partitions(tmp_path)
|
||||
print_summary(size_json, partitions, tmp_path / "firmware.elf")
|
||||
out = capsys.readouterr().out
|
||||
assert "Flash: " in out
|
||||
assert "(used 827455 bytes from 1835008 bytes)" in out
|
||||
|
||||
|
||||
def test_print_summary_flash_line_derives_from_elf(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""A 1.x json without ``total_size`` sums the ELF's loadable PROGBITS
|
||||
sections; NOBITS and non-alloc sections are excluded."""
|
||||
size_json = _write_size_json(tmp_path, _s3_size_data())
|
||||
partitions = _write_partitions(tmp_path)
|
||||
firmware_elf = tmp_path / "firmware.elf"
|
||||
firmware_elf.write_bytes(
|
||||
_elf_bytes(
|
||||
[
|
||||
(1, 0x6, 700000), # PROGBITS, alloc+exec: counted
|
||||
(1, 0x2, 24215), # PROGBITS, alloc: counted
|
||||
(8, 0x2, 50000), # NOBITS (.bss): excluded
|
||||
(1, 0x0, 12345), # PROGBITS, no alloc (.debug_*): excluded
|
||||
]
|
||||
)
|
||||
)
|
||||
print_summary(size_json, partitions, firmware_elf)
|
||||
out = capsys.readouterr().out
|
||||
assert "(used 724215 bytes from 1835008 bytes)" in out
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data",
|
||||
[
|
||||
pytest.param([1, 2], id="top_level_list"),
|
||||
pytest.param({"version": "1.1", "layout": None}, id="layout_null"),
|
||||
pytest.param({"version": "1.1", "layout": 7}, id="layout_scalar"),
|
||||
],
|
||||
)
|
||||
def test_print_summary_handles_unexpected_shapes(
|
||||
data: object, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""A foreign-schema size json degrades to a warning, never a traceback."""
|
||||
size_json = _write_size_json(tmp_path, data)
|
||||
_print_summary_ram_only(tmp_path, size_json)
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
|
||||
def test_print_summary_skips_flash_on_zero_app_partition(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""A zero-size app partition skips the Flash line rather than printing
|
||||
a from-0-bytes figure CI would record."""
|
||||
size_json = _write_size_json(tmp_path, _esp32_size_data())
|
||||
partitions = tmp_path / "partitions.csv"
|
||||
partitions.write_text(
|
||||
"# name, type, subtype, offset, size, flags\napp0, app, ota_0, 0x10000, 0x0,\n"
|
||||
)
|
||||
print_summary(size_json, partitions, tmp_path / "firmware.elf")
|
||||
out = capsys.readouterr().out
|
||||
assert "Flash:" not in out
|
||||
|
||||
|
||||
def test_print_summary_skips_flash_on_unreadable_partitions(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""An unreadable partitions.csv is non-fatal (chmod tricks don't work
|
||||
for root in CI containers, so simulate the OSError instead)."""
|
||||
size_json = _write_size_json(tmp_path, _esp32_size_data())
|
||||
partitions = _write_partitions(tmp_path)
|
||||
with patch(
|
||||
"esphome.espidf.size_summary._find_app_partition_size",
|
||||
side_effect=PermissionError("denied"),
|
||||
):
|
||||
print_summary(size_json, partitions, tmp_path / "firmware.elf")
|
||||
assert "Flash:" not in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_print_summary_flash_falls_back_on_bad_total_size(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""A zero or non-int total_size falls back to the ELF instead of
|
||||
printing a used-0-bytes line CI would read as a real measurement."""
|
||||
data = _s3_size_data()
|
||||
data["total_size"] = 0
|
||||
size_json = _write_size_json(tmp_path, data)
|
||||
partitions = _write_partitions(tmp_path)
|
||||
firmware_elf = tmp_path / "firmware.elf"
|
||||
firmware_elf.write_bytes(_elf_bytes([(1, 0x2, 4096)]))
|
||||
print_summary(size_json, partitions, firmware_elf)
|
||||
out = capsys.readouterr().out
|
||||
assert "(used 4096 bytes from 1835008 bytes)" in out
|
||||
|
||||
|
||||
_GOOD_ELF = _elf_bytes([(1, 0x2, 1024)])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("elf_bytes", "with_partitions"),
|
||||
[
|
||||
pytest.param(None, True, id="missing_elf"),
|
||||
pytest.param(b"junk", True, id="not_an_elf"),
|
||||
pytest.param(
|
||||
_elf_bytes([(1, 0x2, 1024)], shentsize=0), True, id="bad_shentsize"
|
||||
),
|
||||
pytest.param(_GOOD_ELF[:60], True, id="truncated_table"),
|
||||
pytest.param(_elf_bytes([]), True, id="no_sections"),
|
||||
pytest.param(_elf_bytes([(8, 0x2, 50000)]), True, id="no_progbits"),
|
||||
pytest.param(_GOOD_ELF, False, id="missing_partitions"),
|
||||
],
|
||||
)
|
||||
def test_print_summary_skips_flash_on_bad_input(
|
||||
elf_bytes: bytes | None,
|
||||
with_partitions: bool,
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""An unusable ELF or missing partitions.csv skips the Flash line, not the RAM line."""
|
||||
size_json = _write_size_json(tmp_path, _s3_size_data())
|
||||
firmware_elf = tmp_path / "firmware.elf"
|
||||
if elf_bytes is not None:
|
||||
firmware_elf.write_bytes(elf_bytes)
|
||||
if with_partitions:
|
||||
_write_partitions(tmp_path)
|
||||
print_summary(size_json, tmp_path / "partitions.csv", firmware_elf)
|
||||
out = capsys.readouterr().out
|
||||
assert "RAM:" in out
|
||||
assert "Flash:" not in out
|
||||
# ELF problems warn (anomaly after a successful build); a missing
|
||||
# partitions.csv stays at debug
|
||||
warned = any(
|
||||
r.levelname == "WARNING" and "Skipping Flash summary" in r.message
|
||||
for r in caplog.records
|
||||
)
|
||||
assert warned == with_partitions
|
||||
|
||||
Reference in New Issue
Block a user