From ae5b211c8938f294bf1cbd0811b8c5eb0eab8a4e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:30:35 -0400 Subject: [PATCH 01/11] [api] Avoid JsonDocument copy-and-swap operator= in ActionResponse ctor (#16106) --- esphome/components/api/homeassistant_service.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 9d14061d072..aef046fbb04 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -78,7 +78,8 @@ class ActionResponse { : success_(success), error_message_(error_message) { if (data == nullptr || data_len == 0) return; - this->json_document_ = json::parse_json(data, data_len); + JsonDocument tmp = json::parse_json(data, data_len); + swap(this->json_document_, tmp); } #endif From 79da2b9704cb115624697bded6c0294f3b9db528 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:30:46 -0400 Subject: [PATCH 02/11] [time] Fix bugprone-unchecked-optional-access in CronTrigger::check_time_ (#16107) --- esphome/components/time/automation.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 7eb99cfe743..3242669343d 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -31,13 +31,14 @@ void CronTrigger::check_time_() { return; if (this->last_check_.has_value()) { - if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) { + auto &last_check = *this->last_check_; + if (last_check > time && last_check.timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) { // We went back in time (a lot), probably caused by time synchronization ESP_LOGW(TAG, "Time has jumped back!"); - } else if (*this->last_check_ >= time) { + } else if (last_check >= time) { // already handled this one return; - } else if (time > *this->last_check_ && time.timestamp - this->last_check_->timestamp > MAX_TIMESTAMP_DRIFT) { + } else if (time > last_check && time.timestamp - last_check.timestamp > MAX_TIMESTAMP_DRIFT) { // We went ahead in time (a lot), probably caused by time synchronization ESP_LOGW(TAG, "Time has jumped ahead!"); this->last_check_ = time; @@ -45,11 +46,11 @@ void CronTrigger::check_time_() { } while (true) { - this->last_check_->increment_second(); - if (*this->last_check_ >= time) + last_check.increment_second(); + if (last_check >= time) break; - if (this->matches(*this->last_check_)) + if (this->matches(last_check)) this->trigger(); } } From 0a497d3c22be8c4c83466178181f4c6b00179994 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 08:35:17 -0500 Subject: [PATCH 03/11] [light] Fold LightControlAction fields into a single stateless lambda (#16118) --- esphome/components/light/automation.h | 54 ++++----------------- esphome/components/light/automation.py | 67 ++++++++++++++------------ esphome/components/light/types.py | 1 + 3 files changed, 46 insertions(+), 76 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index bc6fd847099..a5c73997b0b 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -31,60 +31,26 @@ template class ToggleAction : public A transition_length_{}; }; -// Unique Empty per field so [[no_unique_address]] is guaranteed to coalesce. -namespace light_control_detail { -template struct Empty {}; -} // namespace light_control_detail - -// X-macro: (type, field_name, bit_index). Order and bit values must match -// the FIELDS table in automation.py. -#define LIGHT_CONTROL_FIELDS(X) \ - X(ColorMode, color_mode, 0) \ - X(bool, state, 1) \ - X(uint32_t, transition_length, 2) \ - X(uint32_t, flash_length, 3) \ - X(float, brightness, 4) \ - X(float, color_brightness, 5) \ - X(float, red, 6) \ - X(float, green, 7) \ - X(float, blue, 8) \ - X(float, white, 9) \ - X(float, color_temperature, 10) \ - X(float, cold_white, 11) \ - X(float, warm_white, 12) \ - X(uint32_t, effect, 13) - -template class LightControlAction : public Action { +// All configured fields are baked into a single stateless lambda whose +// constants live in flash. The action only stores one function pointer +// plus one parent pointer, regardless of how many fields the user set. +// Trigger args are forwarded to the apply function so user lambdas +// (e.g. `brightness: !lambda "return x;"`) keep working. +template class LightControlAction : public Action { public: - explicit LightControlAction(LightState *parent) : parent_(parent) {} - -#define LIGHT_FIELD_SETTER_(type, name, idx) \ - template void set_##name(V value) requires((Fields & (1 << (idx))) != 0) { this->name##_ = value; } -#define LIGHT_FIELD_APPLY_(type, name, idx) \ - if constexpr ((Fields & (1 << (idx))) != 0) \ - call.set_##name(this->name##_.value(x...)); -#define LIGHT_FIELD_DECL_(type, name, idx) \ - [[no_unique_address]] std::conditional_t<(Fields & (1 << (idx))) != 0, TemplatableFn, \ - light_control_detail::Empty<(idx)>> \ - name##_{}; - - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_) + using ApplyFn = void (*)(LightState *, LightCall &, const Ts &...); + LightControlAction(LightState *parent, ApplyFn apply) : parent_(parent), apply_(apply) {} void play(const Ts &...x) override { auto call = this->parent_->make_call(); - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_) + this->apply_(this->parent_, call, x...); call.perform(); } protected: LightState *parent_; - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_DECL_) - -#undef LIGHT_FIELD_DECL_ -#undef LIGHT_FIELD_APPLY_ -#undef LIGHT_FIELD_SETTER_ + ApplyFn apply_; }; -#undef LIGHT_CONTROL_FIELDS template class DimRelativeAction : public Action { public: diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index c666c98e42f..ca4018a9756 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -37,6 +37,7 @@ from .types import ( AddressableSet, ColorMode, DimRelativeAction, + LightCall, LightControlAction, LightIsOffCondition, LightIsOnCondition, @@ -181,8 +182,8 @@ def _resolve_effect_index(config: ConfigType) -> int: async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - # Order/bits must match LIGHT_CONTROL_FIELDS in automation.h. - # EFFECT has special handling below; setter=None skips the generic loop. + # All configured fields are folded into a single stateless lambda whose + # constants live in flash; the action stores only a function pointer. FIELDS = ( (CONF_COLOR_MODE, "set_color_mode", ColorMode), (CONF_STATE, "set_state", cg.bool_), @@ -197,49 +198,51 @@ async def light_control_to_code(config, action_id, template_arg, args): (CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_), (CONF_COLD_WHITE, "set_cold_white", cg.float_), (CONF_WARM_WHITE, "set_warm_white", cg.float_), - (CONF_EFFECT, None, cg.uint32), ) - # Bitmask is passed as uint16_t in C++ — must stay within 16 bits. - assert len(FIELDS) <= 16, "LightControlAction Fields bitmask exceeds uint16_t" - field_mask = sum(1 << i for i, (k, _, _) in enumerate(FIELDS) if k in config) - control_template_arg = cg.TemplateArguments( - cg.RawExpression(f"static_cast({field_mask})"), *template_arg - ) - var = cg.new_Pvariable(action_id, control_template_arg, paren) + fwd_args = ", ".join(name for _, name in args) + body_lines: list[str] = [] for conf_key, setter, type_ in FIELDS: - if conf_key in config and setter is not None: - template_ = await cg.templatable(config[conf_key], args, type_) - cg.add(getattr(var, setter)(template_)) + if conf_key not in config: + continue + value = config[conf_key] + if isinstance(value, Lambda): + inner = await cg.process_lambda(value, args, return_type=type_) + body_lines.append(f"call.{setter}(({inner})({fwd_args}));") + else: + body_lines.append(f"call.{setter}({cg.safe_exp(value)});") if CONF_EFFECT in config: if isinstance(config[CONF_EFFECT], Lambda): - # Lambda returns a string — wrap in a C++ lambda that resolves - # the effect name to its uint32_t index at runtime inner_lambda = await cg.process_lambda( config[CONF_EFFECT], args, return_type=cg.std_string ) - fwd_args = ", ".join(n for _, n in args) - # capture="" is correct: paren is a global variable name - # string-interpolated into the body at codegen time, not a - # C++ runtime capture. - wrapper = LambdaExpression( - f"auto __effect_s = ({inner_lambda})({fwd_args});\n" - f"return {paren}->get_effect_index(" - f"__effect_s.c_str(), __effect_s.size());", - args, - capture="", - return_type=cg.uint32, + body_lines.append( + f"{{ auto __effect_s = ({inner_lambda})({fwd_args});\n" + f"call.set_effect(parent->get_effect_index(" + f"__effect_s.c_str(), __effect_s.size())); }}" ) - cg.add(var.set_effect(wrapper)) else: - # Static string — resolve effect name to index at codegen time - template_ = await cg.templatable( - _resolve_effect_index(config), args, cg.uint32 + # Cast disambiguates between set_effect(uint32_t) and + # set_effect(optional) when the literal is an int. + body_lines.append( + f"call.set_effect(static_cast({_resolve_effect_index(config)}));" ) - cg.add(var.set_effect(template_)) - return var + + # Match LightControlAction::ApplyFn signature: const Ts &... for trigger args. + apply_args = [ + (LightState.operator("ptr"), "parent"), + (LightCall.operator("ref"), "call"), + *((t.operator("const").operator("ref"), n) for t, n in args), + ] + apply_lambda = LambdaExpression( + ["\n".join(body_lines)], + apply_args, + capture="", + return_type=cg.void, + ) + return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda) CONF_RELATIVE_BRIGHTNESS = "relative_brightness" diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index a586bcbd131..534dcd21948 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -13,6 +13,7 @@ Color = cg.esphome_ns.class_("Color") LightColorValues = light_ns.class_("LightColorValues") LightStateRTCState = light_ns.struct("LightStateRTCState") +LightCall = light_ns.class_("LightCall") # Color modes ColorMode = light_ns.enum("ColorMode", is_class=True) From 2bd28eee9d25b30eec64bd71c09c354df9630e65 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:51:31 -0400 Subject: [PATCH 04/11] [tormatic] Use .value() for checked optional access in read_gate_status_ (#16121) --- esphome/components/tormatic/tormatic_cover.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index a58228a219f..cca7b2bba02 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -282,12 +282,13 @@ optional Tormatic::read_gate_status_() { } } + auto hdr = this->pending_hdr_.value(); + // Wait for all payload bytes to arrive before processing. - if (this->available() < this->pending_hdr_->payload_size()) { + if (this->available() < hdr.payload_size()) { return {}; } - auto hdr = *this->pending_hdr_; this->pending_hdr_.reset(); switch (hdr.type) { From 42b8597719f186f8bb5b2469260b1c62f67285a1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:58:19 -0400 Subject: [PATCH 05/11] [api] Extend NOLINT to cover bugprone-random-generator-seed in MAC varint test (#16120) --- tests/components/api/test_proto_mac_varint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/api/test_proto_mac_varint.cpp b/tests/components/api/test_proto_mac_varint.cpp index 317a6fb9d6e..f2a63e96f63 100644 --- a/tests/components/api/test_proto_mac_varint.cpp +++ b/tests/components/api/test_proto_mac_varint.cpp @@ -112,7 +112,7 @@ TEST(ProtoMacVarint, AllOnes) { verify_mac(0xFFFFFFFFFFFFULL, 7); } // F // 100 deterministic-random 48-bit MACs to catch regressions across the space. TEST(ProtoMacVarint, RandomSample) { - // NOLINTNEXTLINE(cert-msc32-c,cert-msc51-cpp) -- intentional fixed seed for reproducibility. + // NOLINTNEXTLINE(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) -- fixed seed for reproducibility std::mt19937_64 rng(0xC0FFEE); for (int i = 0; i < 100; i++) { uint64_t mac = rng() & 0xFFFFFFFFFFFFULL; From 2157d1191372b74bed4df5b8a371d5fb1dfeebe7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:26:53 -0400 Subject: [PATCH 06/11] [haier] Fix bugprone-unchecked-optional-access; switch HardwareInfo to char[9] (#16124) --- esphome/components/haier/hon_climate.cpp | 48 ++++++++----------- esphome/components/haier/hon_climate.h | 13 ++--- .../components/haier/smartair2_climate.cpp | 2 +- 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 1e9cb42f382..87b8add2a37 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -85,7 +85,7 @@ void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direct this->force_send_control_ = true; } -std::string HonClimate::get_cleaning_status_text() const { +const char *HonClimate::get_cleaning_status_text() const { switch (this->cleaning_status_) { case CleaningState::SELF_CLEAN: return "Self clean"; @@ -134,29 +134,22 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haie } // All OK hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data; - char tmp[9]; - tmp[8] = 0; - strncpy(tmp, answr->protocol_version, 8); - this->hvac_hardware_info_ = HardwareInfo(); - this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp); - strncpy(tmp, answr->software_version, 8); - this->hvac_hardware_info_.value().software_version_ = std::string(tmp); - strncpy(tmp, answr->hardware_version, 8); - this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp); - strncpy(tmp, answr->device_name, 8); - this->hvac_hardware_info_.value().device_name_ = std::string(tmp); + HardwareInfo info{}; // zero-init guarantees null-termination + strncpy(info.protocol_version_, answr->protocol_version, HARDWARE_INFO_STR_SIZE - 1); + strncpy(info.software_version_, answr->software_version, HARDWARE_INFO_STR_SIZE - 1); + strncpy(info.hardware_version_, answr->hardware_version, HARDWARE_INFO_STR_SIZE - 1); + strncpy(info.device_name_, answr->device_name, HARDWARE_INFO_STR_SIZE - 1); + info.functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support + info.functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support + info.functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support + info.functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support + info.functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support + this->use_crc_ = info.functions_[2]; #ifdef USE_TEXT_SENSOR - this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, this->hvac_hardware_info_.value().device_name_); - this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION, - this->hvac_hardware_info_.value().protocol_version_); + this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, info.device_name_); + this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION, info.protocol_version_); #endif - this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support - this->hvac_hardware_info_.value().functions_[1] = - (answr->functions[1] & 0x02) != 0; // controller-device mode support - this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support - this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support - this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support - this->use_crc_ = this->hvac_hardware_info_.value().functions_[2]; + this->hvac_hardware_info_ = info; this->set_phase(ProtocolPhases::SENDING_INIT_2); return result; } else { @@ -347,10 +340,9 @@ void HonClimate::dump_config() { " Device software version: %s\n" " Device hardware version: %s\n" " Device name: %s", - this->hvac_hardware_info_.value().protocol_version_.c_str(), - this->hvac_hardware_info_.value().software_version_.c_str(), - this->hvac_hardware_info_.value().hardware_version_.c_str(), - this->hvac_hardware_info_.value().device_name_.c_str()); + this->hvac_hardware_info_.value().protocol_version_, + this->hvac_hardware_info_.value().software_version_, + this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_); ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""), (this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""), @@ -460,7 +452,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { if (this->action_request_.has_value()) { if (this->action_request_.value().message.has_value()) { this->send_message_(this->action_request_.value().message.value(), this->use_crc_); - this->action_request_.value().message.reset(); + this->action_request_.value().message.reset(); // NOLINT(bugprone-unchecked-optional-access) } else { // Message already sent, reseting request and return to idle this->action_request_.reset(); @@ -796,7 +788,7 @@ void HonClimate::set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSe } } -void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::string &value) { +void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const char *value) { size_t index = (size_t) type; if (this->sub_text_sensors_[index] != nullptr) this->sub_text_sensors_[index]->publish_state(value); diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 7a87f27b668..a0bcdfb548c 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -90,7 +90,7 @@ class HonClimate : public HaierClimateBase { void set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSensor *sens); protected: - void update_sub_text_sensor_(SubTextSensorType type, const std::string &value); + void update_sub_text_sensor_(SubTextSensorType type, const char *value); text_sensor::TextSensor *sub_text_sensors_[(size_t) SubTextSensorType::SUB_TEXT_SENSOR_TYPE_COUNT]{nullptr}; #endif #ifdef USE_SWITCH @@ -116,7 +116,7 @@ class HonClimate : public HaierClimateBase { void set_vertical_airflow(hon_protocol::VerticalSwingMode direction); esphome::optional get_horizontal_airflow() const; void set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction); - std::string get_cleaning_status_text() const; + const char *get_cleaning_status_text() const; CleaningState get_cleaning_status() const; void start_self_cleaning(); void start_steri_cleaning(); @@ -166,11 +166,12 @@ class HonClimate : public HaierClimateBase { void fill_control_messages_queue_(); void clear_control_messages_queue_(); + static constexpr size_t HARDWARE_INFO_STR_SIZE = 9; struct HardwareInfo { - std::string protocol_version_; - std::string software_version_; - std::string hardware_version_; - std::string device_name_; + char protocol_version_[HARDWARE_INFO_STR_SIZE]; + char software_version_[HARDWARE_INFO_STR_SIZE]; + char hardware_version_[HARDWARE_INFO_STR_SIZE]; + char device_name_[HARDWARE_INFO_STR_SIZE]; bool functions_[5]; }; diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 200cac25575..752f4d4f1c0 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -191,7 +191,7 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) if (this->action_request_.has_value()) { if (this->action_request_.value().message.has_value()) { this->send_message_(this->action_request_.value().message.value(), this->use_crc_); - this->action_request_.value().message.reset(); + this->action_request_.value().message.reset(); // NOLINT(bugprone-unchecked-optional-access) } else { // Message already sent, reseting request and return to idle this->action_request_.reset(); From bacee89bca9ebde55687d746d75985516cc43c0d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:56:13 -0400 Subject: [PATCH 07/11] [mixer_speaker] NOLINT bugprone-unchecked-optional-access in audio_mixer_task (#16130) --- esphome/components/mixer/speaker/mixer_speaker.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 741239a2dd9..0d16bce330b 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -588,6 +588,7 @@ void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::Audio } } +// NOLINTBEGIN(bugprone-unchecked-optional-access) -- audio_stream_info_ always set before this task is created void MixerSpeaker::audio_mixer_task(void *params) { MixerSpeaker *this_mixer = static_cast(params); @@ -764,6 +765,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } +// NOLINTEND(bugprone-unchecked-optional-access) } // namespace esphome::mixer_speaker From 557c3d443611eb8443959ebf1b15206a2030070f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:33:29 -0400 Subject: [PATCH 08/11] [aqi] Use std::max initializer-list for non-negative AQI clamp (#16134) --- esphome/components/aqi/aqi_calculator.h | 6 +----- esphome/components/aqi/caqi_calculator.h | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/esphome/components/aqi/aqi_calculator.h b/esphome/components/aqi/aqi_calculator.h index d624af0432f..bb8e4022802 100644 --- a/esphome/components/aqi/aqi_calculator.h +++ b/esphome/components/aqi/aqi_calculator.h @@ -14,11 +14,7 @@ class AQICalculator : public AbstractAQICalculator { uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - - float aqi = std::max(pm2_5_index, pm10_0_index); - if (aqi < 0.0f) { - aqi = 0.0f; - } + float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f}); return static_cast(std::lround(aqi)); } diff --git a/esphome/components/aqi/caqi_calculator.h b/esphome/components/aqi/caqi_calculator.h index fe2efe7059d..3f6da45aa91 100644 --- a/esphome/components/aqi/caqi_calculator.h +++ b/esphome/components/aqi/caqi_calculator.h @@ -12,11 +12,7 @@ class CAQICalculator : public AbstractAQICalculator { uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - - float aqi = std::max(pm2_5_index, pm10_0_index); - if (aqi < 0.0f) { - aqi = 0.0f; - } + float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f}); return static_cast(std::lround(aqi)); } From bae6b516523a4814ae49f1fbeb3a964b9b192a5b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:33:57 -0400 Subject: [PATCH 09/11] [kamstrup_kmp][toshiba] Fix signed/unsigned comparisons against sizeof (#16135) --- esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 6 +++--- esphome/components/toshiba/toshiba.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 9f2557243c8..9bebd4cd56d 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -139,12 +139,12 @@ void KamstrupKMPComponent::clear_uart_rx_buffer_() { void KamstrupKMPComponent::read_command_(uint16_t command) { uint8_t buffer[20] = {0}; - int buffer_len = 0; + size_t buffer_len = 0; int data; int timeout = 250; // ms // Read the data from the UART - while (timeout > 0 && buffer_len < static_cast(sizeof(buffer))) { + while (timeout > 0 && buffer_len < sizeof(buffer)) { if (this->available()) { data = this->read(); if (data > -1) { @@ -183,7 +183,7 @@ void KamstrupKMPComponent::read_command_(uint16_t command) { // Decode uint8_t msg[20] = {0}; int msg_len = 0; - for (int i = 1; i < buffer_len - 1; i++) { + for (size_t i = 1; i < buffer_len - 1; i++) { if (buffer[i] == 0x1B) { msg[msg_len++] = buffer[i + 1] ^ 0xFF; i++; diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 53114cc50fc..a23b4c7cc3e 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -275,7 +275,7 @@ static Ras2819tSecondPacketCodes get_ras_2819t_second_packet_codes(climate::Clim */ static uint8_t get_ras_2819t_temp_code(float temperature) { int temp_index = static_cast(temperature) - 18; - if (temp_index < 0 || temp_index >= static_cast(sizeof(RAS_2819T_TEMP_CODES))) { + if (temp_index < 0 || static_cast(temp_index) >= sizeof(RAS_2819T_TEMP_CODES)) { ESP_LOGW(TAG, "Temperature %.1f°C out of range [18-30°C], defaulting to 24°C", temperature); return 0x40; // Default to 24°C } From ce61dcf3873b7fe7a7e8063cc44168222a3aee4a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:54:17 -0400 Subject: [PATCH 10/11] [remote_base][core] Drop redundant typename in dependent type contexts (#16137) --- esphome/components/remote_base/remote_base.h | 8 ++++---- esphome/core/finite_set_mask.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index d73fff2b0a5..e5e923d7806 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -164,7 +164,7 @@ class RemoteTransmitterBase : public RemoteComponentBase { return TransmitCall(this); } template - void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { auto call = this->transmit(); Protocol().encode(call.get_data(), data); call.set_send_times(send_times); @@ -250,10 +250,10 @@ template class RemoteReceiverBinarySensor : public RemoteReceiverBin } public: - void set_data(typename T::ProtocolData data) { data_ = data; } + void set_data(T::ProtocolData data) { data_ = data; } protected: - typename T::ProtocolData data_; + T::ProtocolData data_; }; template @@ -278,7 +278,7 @@ class RemoteTransmittable { protected: template - void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + void transmit_(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { this->transmitter_->transmit(data, send_times, send_wait); } RemoteTransmitterBase *transmitter_; diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index 616c69353d7..272337ff76f 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -55,7 +55,7 @@ template struct DefaultBitPolicy { /// template> class FiniteSetMask { public: - using bitmask_t = typename BitPolicy::mask_t; + using bitmask_t = BitPolicy::mask_t; constexpr FiniteSetMask() = default; From 69a33d8ac0c879646923767b7cc2164b69c838c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 12:31:55 -0500 Subject: [PATCH 11/11] [core] Inline HAL clock wrappers and split hal.h into per-platform headers (#15977) --- esphome/components/esp32/core.cpp | 11 +-- esphome/components/esp8266/core.cpp | 17 +--- esphome/components/libretiny/core.cpp | 28 +----- esphome/components/rp2040/core.cpp | 6 +- esphome/core/hal.h | 131 ++++++-------------------- esphome/core/hal/hal_esp32.h | 35 +++++++ esphome/core/hal/hal_esp8266.h | 65 +++++++++++++ esphome/core/hal/hal_host.h | 24 +++++ esphome/core/hal/hal_libretiny.h | 93 ++++++++++++++++++ esphome/core/hal/hal_rp2040.h | 40 ++++++++ esphome/core/hal/hal_zephyr.h | 24 +++++ esphome/core/helpers.h | 41 +------- esphome/core/time_64.h | 6 +- esphome/core/time_conversion.h | 46 +++++++++ 14 files changed, 366 insertions(+), 201 deletions(-) create mode 100644 esphome/core/hal/hal_esp32.h create mode 100644 esphome/core/hal/hal_esp8266.h create mode 100644 esphome/core/hal/hal_host.h create mode 100644 esphome/core/hal/hal_libretiny.h create mode 100644 esphome/core/hal/hal_rp2040.h create mode 100644 esphome/core/hal/hal_zephyr.h create mode 100644 esphome/core/time_conversion.h diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 1c631371836..4886745c068 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -22,7 +22,7 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { -void HOT yield() { vPortYield(); } +// yield(), delay(), micros(), millis_64() inlined in hal.h. // Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig), // falling back to esp_timer for non-standard rates. IRAM_ATTR is required because // Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32. @@ -37,15 +37,6 @@ uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast(esp_timer_get_time())); #endif } -// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is -// safe because the two are never cross-compared: millis() values are only used for -// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while -// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME), -// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly, -// so the Scheduler is internally consistent on the esp_timer clock. -uint64_t HOT millis_64() { return micros_to_millis(static_cast(esp_timer_get_time())); } -void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } -uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { esp_restart(); diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index c9bedb61be4..9161ca6aaf5 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -15,7 +15,7 @@ extern "C" { namespace esphome { -void HOT yield() { ::yield(); } +// yield(), micros(), millis_64() inlined in hal.h. // Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit // multiplies on the LX106). Tracks a running ms counter from 32-bit // system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis @@ -66,7 +66,6 @@ uint32_t IRAM_ATTR HOT millis() { xt_wsr_ps(ps); return result; } -uint64_t millis_64() { return Millis64Impl::compute(millis()); } // Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object // call to the original millis() that --wrap can't intercept, so calling ::delay() // would keep the slow Arduino millis body alive in IRAM. optimistic_yield still @@ -85,8 +84,7 @@ void HOT delay(uint32_t ms) { optimistic_yield(1000); } } -uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +// delayMicroseconds(), arch_feed_wdt(), and progmem_read_*() are inlined in hal/hal_esp8266.h. void arch_restart() { system_restart(); // restart() doesn't always end execution @@ -95,17 +93,6 @@ void arch_restart() { } } void arch_init() {} -void HOT arch_feed_wdt() { system_soft_wdt_feed(); } - -uint8_t progmem_read_byte(const uint8_t *addr) { - return pgm_read_byte(addr); // NOLINT -} -const char *progmem_read_ptr(const char *const *addr) { - return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT -} -uint16_t progmem_read_uint16(const uint16_t *addr) { - return pgm_read_word(addr); // NOLINT -} uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return F_CPU; } diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index ca46bcb899e..f46abe3b81a 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -3,7 +3,6 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/time_64.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -15,32 +14,7 @@ void loop(); namespace esphome { -void HOT yield() { ::yield(); } -// Inline the tick read so esphome::millis() matches MillisInternal::get()'s fast -// path instead of going through the Arduino core's out-of-line ::millis() wrapper. -// -// RTL87xx / LN882x (1 kHz): xTaskGetTickCount() is already ms. IRAM_ATTR + ISR -// dispatch are needed because ISR handlers (e.g. rotary_encoder) call millis(). -// -// BK72xx (500 Hz): ticks * portTICK_PERIOD_MS (== 2). IRAM_ATTR and ISR dispatch -// are both unnecessary — the SDK masks FIQ + IRQ during flash writes (see hal.h), -// so no ISR runs while flash is stalled. -#if defined(USE_RTL87XX) || defined(USE_LN882X) -uint32_t IRAM_ATTR HOT millis() { - static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick"); - return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount(); -} -#elif defined(USE_BK72XX) -uint32_t HOT millis() { - static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick"); - return xTaskGetTickCount() * portTICK_PERIOD_MS; -} -#else -uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -#endif -uint64_t millis_64() { return Millis64Impl::compute(millis()); } -uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void HOT delay(uint32_t ms) { ::delay(ms); } +// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h. void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } void arch_init() { diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index b7a90006123..d3dc1cf2bb5 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -13,11 +13,7 @@ namespace esphome { -void HOT yield() { ::yield(); } -uint64_t millis_64() { return micros_to_millis(time_us_64()); } -uint32_t HOT millis() { return micros_to_millis(time_us_64()); } -void HOT delay(uint32_t ms) { ::delay(ms); } -uint32_t HOT micros() { return ::micros(); } +// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h. void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { watchdog_reboot(0, 0, 10); diff --git a/esphome/core/hal.h b/esphome/core/hal.h index e4083622b98..e20797cf95d 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -2,125 +2,48 @@ #include #include #include "gpio.h" +#include "esphome/core/defines.h" +#include "esphome/core/time_64.h" +#include "esphome/core/time_conversion.h" +// Per-platform HAL bits (IRAM_ATTR / PROGMEM macros, in_isr_context(), +// inline yield/delay/micros/millis/millis_64 wrappers, ESP8266 progmem +// helpers) live under esphome/core/hal/ and are dispatched here based on +// the active USE_* platform define. Each header guards its body with the +// matching #ifdef USE_ and re-enters namespace esphome {} so it +// is safe to be re-included. #if defined(USE_ESP32) -#include -#ifndef PROGMEM -#define PROGMEM -#endif - +#include "esphome/core/hal/hal_esp32.h" #elif defined(USE_ESP8266) - -#include -#ifndef PROGMEM -#define PROGMEM ICACHE_RODATA_ATTR -#endif - -#elif defined(USE_RP2040) - -#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical"))) -#define PROGMEM - +#include "esphome/core/hal/hal_esp8266.h" #elif defined(USE_LIBRETINY) - -// IRAM_ATTR places a function in executable RAM so it is callable from an -// ISR even while flash is busy (XIP stall, OTA, logger flash write). -// Each family uses a section its stock linker already routes to RAM: -// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the -// exception: its stock linker has no matching glob, so patch_linker.py -// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link. -// -// BK72xx (all variants) are left as a no-op: their SDK wraps flash -// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for -// the duration of every write, so no ISR fires while flash is stalled and -// the race IRAM_ATTR guards against cannot occur. The trade-off is that -// interrupts are delayed (not dropped) by up to ~20 ms during a sector -// erase, but that is an SDK-level choice and cannot be changed from this -// layer. -#if defined(USE_BK72XX) -#define IRAM_ATTR -#elif defined(USE_LIBRETINY_VARIANT_RTL8710B) -// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM). -#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text"))) +#include "esphome/core/hal/hal_libretiny.h" +#elif defined(USE_RP2040) +#include "esphome/core/hal/hal_rp2040.h" +#elif defined(USE_HOST) +#include "esphome/core/hal/hal_host.h" +#elif defined(USE_ZEPHYR) +#include "esphome/core/hal/hal_zephyr.h" #else -// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text. -// LN882H: patch_linker.py.script injects *(.sram.text*) into -// .flash_copysection (> RAM0 AT> FLASH). -#define IRAM_ATTR __attribute__((noinline, section(".sram.text"))) -#endif -#define PROGMEM - -#else - -#define IRAM_ATTR -#define PROGMEM - -#endif - -#ifdef USE_ESP32 -#include -#include -#endif - -#ifdef USE_BK72XX -// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so -// it is callable from Thumb code via interworking. The MRS CPSR instruction -// is ARM-only and user code here may be built in Thumb, so in_isr_context() -// defers to this port helper on BK72xx instead of reading CPSR inline. -extern "C" uint32_t platform_is_in_interrupt_context(void); +#error "hal.h: not implemented for this platform" #endif namespace esphome { -/// Returns true when executing inside an interrupt handler. -/// always_inline so callers placed in IRAM keep the detection in IRAM. -__attribute__((always_inline)) inline bool in_isr_context() { -#if defined(USE_ESP32) - return xPortInIsrContext() != 0; -#elif defined(USE_ESP8266) - // ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is - // non-zero both in a real ISR and when user code masks interrupts. The - // ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule - // which is ISR-safe) so this helper is unused on this platform. - return false; -#elif defined(USE_RP2040) - uint32_t ipsr; - __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); - return ipsr != 0; -#elif defined(USE_BK72XX) - // BK72xx is ARM968E-S (ARM9); see extern declaration above. - return platform_is_in_interrupt_context() != 0; -#elif defined(USE_LIBRETINY) - // Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number; - // non-zero means we're in a handler. - uint32_t ipsr; - __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); - return ipsr != 0; -#else - // Host and any future platform without an ISR concept. - return false; -#endif -} - -void yield(); -uint32_t millis(); -uint64_t millis_64(); -uint32_t micros(); -void delay(uint32_t ms); +// ESP8266 inlines delayMicroseconds() and arch_feed_wdt() in hal/hal_esp8266.h; +// every other platform keeps them out-of-line in components//core.cpp. +#ifndef USE_ESP8266 void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) +void arch_feed_wdt(); +#endif void __attribute__((noreturn)) arch_restart(); void arch_init(); -void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); -#ifdef USE_ESP8266 -// ESP8266: pgm_read_* does real flash reads on Harvard architecture -uint8_t progmem_read_byte(const uint8_t *addr); -const char *progmem_read_ptr(const char *const *addr); -uint16_t progmem_read_uint16(const uint16_t *addr); -#else -// All other platforms: PROGMEM is a no-op, so these are direct dereferences +#ifndef USE_ESP8266 +// All non-ESP8266 platforms: PROGMEM is a no-op, so these are direct dereferences. +// ESP8266's out-of-line declarations live in hal/hal_esp8266.h. inline uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } inline const char *progmem_read_ptr(const char *const *addr) { return *addr; } inline uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } diff --git a/esphome/core/hal/hal_esp32.h b/esphome/core/hal/hal_esp32.h new file mode 100644 index 00000000000..e755337540d --- /dev/null +++ b/esphome/core/hal/hal_esp32.h @@ -0,0 +1,35 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include +#include +#include + +#include "esphome/core/time_conversion.h" + +#ifndef PROGMEM +#define PROGMEM +#endif + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +__attribute__((always_inline)) inline bool in_isr_context() { return xPortInIsrContext() != 0; } + +// Forward decl from . +// NOLINTNEXTLINE(readability-redundant-declaration) +extern "C" int64_t esp_timer_get_time(void); + +__attribute__((always_inline)) inline void yield() { vPortYield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(esp_timer_get_time()); } +uint32_t millis(); +__attribute__((always_inline)) inline uint64_t millis_64() { + return micros_to_millis(static_cast(esp_timer_get_time())); +} + +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/core/hal/hal_esp8266.h b/esphome/core/hal/hal_esp8266.h new file mode 100644 index 00000000000..04326a3579c --- /dev/null +++ b/esphome/core/hal/hal_esp8266.h @@ -0,0 +1,65 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include +#include +#include + +#include "esphome/core/time_64.h" + +#ifndef PROGMEM +#define PROGMEM ICACHE_RODATA_ATTR +#endif + +// Forward decls from Arduino's for the inline wrappers below. +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" void yield(void); +extern "C" void delay(unsigned long ms); +extern "C" unsigned long micros(void); +extern "C" unsigned long millis(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) + +// Forward decl from for arch_feed_wdt() inline below. +// NOLINTNEXTLINE(readability-redundant-declaration) +extern "C" void system_soft_wdt_feed(void); + +namespace esphome { + +// Forward decl from helpers.h so this header stays cheap. +// NOLINTNEXTLINE(readability-redundant-declaration) +void delay_microseconds_safe(uint32_t us); + +/// Returns true when executing inside an interrupt handler. +/// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is +/// non-zero both in a real ISR and when user code masks interrupts. The +/// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule +/// which is ISR-safe) so this helper is unused on this platform. +__attribute__((always_inline)) inline bool in_isr_context() { return false; } + +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } +void delay(uint32_t ms); +uint32_t millis(); +__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } + +// ESP8266: pgm_read_* does aligned 32-bit flash reads on Harvard architecture. +// Inline-forward to the platform macros so the wrappers themselves don't +// occupy IRAM/flash on every call site. +__attribute__((always_inline)) inline uint8_t progmem_read_byte(const uint8_t *addr) { + return pgm_read_byte(addr); // NOLINT +} +__attribute__((always_inline)) inline const char *progmem_read_ptr(const char *const *addr) { + return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT +} +__attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_t *addr) { + return pgm_read_word(addr); // NOLINT +} + +// NOLINTNEXTLINE(readability-identifier-naming) +__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +__attribute__((always_inline)) inline void arch_feed_wdt() { system_soft_wdt_feed(); } + +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/core/hal/hal_host.h b/esphome/core/hal/hal_host.h new file mode 100644 index 00000000000..145fe4ea9c1 --- /dev/null +++ b/esphome/core/hal/hal_host.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef USE_HOST + +#include + +#define IRAM_ATTR +#define PROGMEM + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +/// Host has no ISR concept. +__attribute__((always_inline)) inline bool in_isr_context() { return false; } + +void yield(); +void delay(uint32_t ms); +uint32_t micros(); +uint32_t millis(); +uint64_t millis_64(); + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/core/hal/hal_libretiny.h b/esphome/core/hal/hal_libretiny.h new file mode 100644 index 00000000000..e0d92735bbb --- /dev/null +++ b/esphome/core/hal/hal_libretiny.h @@ -0,0 +1,93 @@ +#pragma once + +#ifdef USE_LIBRETINY + +#include + +// For the inline millis() fast paths (xTaskGetTickCount, portTICK_PERIOD_MS). +#include +#include + +#include "esphome/core/time_64.h" + +// IRAM_ATTR places a function in executable RAM so it is callable from an +// ISR even while flash is busy (XIP stall, OTA, logger flash write). +// Each family uses a section its stock linker already routes to RAM: +// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the +// exception: its stock linker has no matching glob, so patch_linker.py +// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link. +// +// BK72xx (all variants) are left as a no-op: their SDK wraps flash +// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for +// the duration of every write, so no ISR fires while flash is stalled and +// the race IRAM_ATTR guards against cannot occur. The trade-off is that +// interrupts are delayed (not dropped) by up to ~20 ms during a sector +// erase, but that is an SDK-level choice and cannot be changed from this +// layer. +#if defined(USE_BK72XX) +#define IRAM_ATTR +#elif defined(USE_LIBRETINY_VARIANT_RTL8710B) +// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM). +#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text"))) +#else +// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text. +// LN882H: patch_linker.py.script injects *(.sram.text*) into +// .flash_copysection (> RAM0 AT> FLASH). +#define IRAM_ATTR __attribute__((noinline, section(".sram.text"))) +#endif +#define PROGMEM + +#ifdef USE_BK72XX +// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so +// it is callable from Thumb code via interworking. The MRS CPSR instruction +// is ARM-only and user code here may be built in Thumb, so in_isr_context() +// defers to this port helper on BK72xx instead of reading CPSR inline. +extern "C" uint32_t platform_is_in_interrupt_context(void); +#endif + +// Forward decls from Arduino's for the inline wrappers below. +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" void yield(void); +extern "C" void delay(unsigned long ms); +extern "C" unsigned long micros(void); +extern "C" unsigned long millis(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +__attribute__((always_inline)) inline bool in_isr_context() { +#if defined(USE_BK72XX) + // BK72xx is ARM968E-S (ARM9); see extern declaration above. + return platform_is_in_interrupt_context() != 0; +#else + // Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number; + // non-zero means we're in a handler. + uint32_t ipsr; + __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); + return ipsr != 0; +#endif +} + +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } + +// Per-variant millis() fast path — matches MillisInternal::get(). +#if defined(USE_RTL87XX) || defined(USE_LN882X) +static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick"); +__attribute__((always_inline)) inline uint32_t millis() { + // xTaskGetTickCountFromISR is mandatory in interrupt context per the FreeRTOS API contract. + return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount(); +} +#elif defined(USE_BK72XX) +static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick"); +__attribute__((always_inline)) inline uint32_t millis() { return xTaskGetTickCount() * portTICK_PERIOD_MS; } +#else +__attribute__((always_inline)) inline uint32_t millis() { return static_cast(::millis()); } +#endif +__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } + +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/core/hal/hal_rp2040.h b/esphome/core/hal/hal_rp2040.h new file mode 100644 index 00000000000..156ff33b863 --- /dev/null +++ b/esphome/core/hal/hal_rp2040.h @@ -0,0 +1,40 @@ +#pragma once + +#ifdef USE_RP2040 + +#include + +#include "esphome/core/time_conversion.h" + +#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical"))) +#define PROGMEM + +// Forward decls from Arduino's for the inline wrappers below. +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" void yield(void); +extern "C" void delay(unsigned long ms); +extern "C" unsigned long micros(void); +extern "C" unsigned long millis(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) + +// Forward decl from . +extern "C" uint64_t time_us_64(void); + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +__attribute__((always_inline)) inline bool in_isr_context() { + uint32_t ipsr; + __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); + return ipsr != 0; +} + +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } +__attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); } +__attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis(::time_us_64()); } + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/core/hal/hal_zephyr.h b/esphome/core/hal/hal_zephyr.h new file mode 100644 index 00000000000..e28be5c775d --- /dev/null +++ b/esphome/core/hal/hal_zephyr.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef USE_ZEPHYR + +#include + +#define IRAM_ATTR +#define PROGMEM + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +/// Zephyr/nRF52: not currently consulted — wake path is platform-specific. +__attribute__((always_inline)) inline bool in_isr_context() { return false; } + +void yield(); +void delay(uint32_t ms); +uint32_t micros(); +uint32_t millis(); +uint64_t millis_64(); + +} // namespace esphome + +#endif // USE_ZEPHYR diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b2b07c57a05..355db6c7f4f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -20,6 +20,7 @@ #include #include "esphome/core/optional.h" +#include "esphome/core/time_conversion.h" // Backward compatibility re-export of heap-allocating helpers. // These functions have moved to alloc_helpers.h. External components should @@ -833,43 +834,9 @@ template constexpr uint32_t fnv1a_hash_extend(uint32_t hash, T constexpr uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); } inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); } -/// Convert a 64-bit microsecond count to milliseconds without calling -/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz). -/// -/// Returns uint32_t by default (for millis()), or uint64_t when requested -/// (for millis_64()). The only difference is whether hi * Q is truncated -/// to 32 bits or widened to 64. -/// -/// On 32-bit targets, GCC does not optimize 64-bit constant division into a -/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3 -/// (free divide-by-8), then use the Euclidean division identity to decompose -/// the remaining 64-bit divide-by-125 into a single 32-bit division: -/// -/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers] -/// 2^32 = Q * 125 + R (34359738 * 125 + 46) -/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125 -/// -/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal -/// (mulhu + shift), so no division instruction is emitted. -/// -/// Safe for us up to ~3.2e18 (~101,700 years of microseconds). -/// -/// See: https://en.wikipedia.org/wiki/Euclidean_division -/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html -template inline constexpr ESPHOME_ALWAYS_INLINE ReturnT micros_to_millis(uint64_t us) { - constexpr uint32_t d = 125U; - constexpr uint32_t q = static_cast((1ULL << 32) / d); // 34359738 - constexpr uint32_t r = static_cast((1ULL << 32) % d); // 46 - // 1000 = 8 * 125; divide-by-8 is a free shift - uint64_t x = us >> 3; - uint32_t lo = static_cast(x); - uint32_t hi = static_cast(x >> 32); - // Combine remainder term: hi * (2^32 % 125) + lo - uint32_t adj = hi * r + lo; - // If adj overflowed, the true value is 2^32 + adj; apply the identity again - // static_cast(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q - return static_cast(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d); -} +// micros_to_millis<>() lives in its own lightweight header so hal.h can pull it +// in for inline millis_64() without forcing every TU that includes hal.h to +// also include the rest of helpers.h. /// Return a random 32-bit unsigned integer. /// Not thread-safe. Must only be called from the main loop. diff --git a/esphome/core/time_64.h b/esphome/core/time_64.h index d82373dbfe9..f66f9afddb8 100644 --- a/esphome/core/time_64.h +++ b/esphome/core/time_64.h @@ -6,8 +6,6 @@ #include #include -#include "esphome/core/helpers.h" - namespace esphome { class Scheduler; @@ -24,7 +22,9 @@ class Millis64Impl { static uint32_t last_millis; static uint16_t millis_major; - static inline uint64_t ESPHOME_ALWAYS_INLINE compute(uint32_t now) { + // Raw __attribute__((always_inline)) (not ESPHOME_ALWAYS_INLINE) so this + // header does not need to pull helpers.h. + static inline uint64_t __attribute__((always_inline)) compute(uint32_t now) { // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; diff --git a/esphome/core/time_conversion.h b/esphome/core/time_conversion.h new file mode 100644 index 00000000000..e9060c06267 --- /dev/null +++ b/esphome/core/time_conversion.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +namespace esphome { + +/// Convert a 64-bit microsecond count to milliseconds without calling +/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz). +/// +/// Returns uint32_t by default (for millis()), or uint64_t when requested +/// (for millis_64()). The only difference is whether hi * Q is truncated +/// to 32 bits or widened to 64. +/// +/// On 32-bit targets, GCC does not optimize 64-bit constant division into a +/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3 +/// (free divide-by-8), then use the Euclidean division identity to decompose +/// the remaining 64-bit divide-by-125 into a single 32-bit division: +/// +/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers] +/// 2^32 = Q * 125 + R (34359738 * 125 + 46) +/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125 +/// +/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal +/// (mulhu + shift), so no division instruction is emitted. +/// +/// Safe for us up to ~3.2e18 (~101,700 years of microseconds). +/// +/// See: https://en.wikipedia.org/wiki/Euclidean_division +/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html +template +__attribute__((always_inline)) inline constexpr ReturnT micros_to_millis(uint64_t us) { + constexpr uint32_t d = 125U; + constexpr uint32_t q = static_cast((1ULL << 32) / d); // 34359738 + constexpr uint32_t r = static_cast((1ULL << 32) % d); // 46 + // 1000 = 8 * 125; divide-by-8 is a free shift + uint64_t x = us >> 3; + uint32_t lo = static_cast(x); + uint32_t hi = static_cast(x >> 32); + // Combine remainder term: hi * (2^32 % 125) + lo + uint32_t adj = hi * r + lo; + // If adj overflowed, the true value is 2^32 + adj; apply the identity again + // static_cast(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q + return static_cast(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d); +} + +} // namespace esphome