mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 09:08:41 +00:00
Merge branch 'refactor/sliding-window-static-ring-buffer' into integration
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e
|
||||
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
#endif // CYW43_USES_VSYS_PIN
|
||||
#include <hardware/adc.h>
|
||||
|
||||
// PICO_VSYS_PIN is defined in pico-sdk board headers (e.g. boards/pico2.h),
|
||||
// but the Arduino framework's config_autogen.h includes a generic board header
|
||||
// that doesn't define it. Provide the standard value (pin 29) as a fallback.
|
||||
#ifndef PICO_VSYS_PIN
|
||||
#define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage)
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
|
||||
@@ -214,4 +214,4 @@ async def to_code(config):
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT")
|
||||
if data.opus_support:
|
||||
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.3.4")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.3.5")
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
#include <driver/ledc.h>
|
||||
#include <cinttypes>
|
||||
#include <esp_private/periph_ctrl.h>
|
||||
#if !defined(SOC_LEDC_SUPPORT_FADE_STOP)
|
||||
#include <hal/ledc_ll.h>
|
||||
#endif
|
||||
|
||||
#define CLOCK_FREQUENCY 80e6f
|
||||
|
||||
@@ -16,10 +20,10 @@
|
||||
|
||||
static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5;
|
||||
|
||||
namespace esphome {
|
||||
namespace ledc {
|
||||
namespace esphome::ledc {
|
||||
|
||||
static const char *const TAG = "ledc.output";
|
||||
static bool ledc_peripheral_reset_done = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1;
|
||||
#if SOC_LEDC_SUPPORT_HS_MODE
|
||||
@@ -32,6 +36,28 @@ inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_H
|
||||
inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; }
|
||||
#endif
|
||||
|
||||
#if !defined(SOC_LEDC_SUPPORT_FADE_STOP)
|
||||
// Classic ESP32 (currently the only target without SOC_LEDC_SUPPORT_FADE_STOP) can block in
|
||||
// ledc_ll_set_duty_start() while duty_start is set. We check the same conf1.duty_start bit here
|
||||
// to defer updates and avoid entering IDF's unbounded wait loop.
|
||||
//
|
||||
// This intentionally depends on the classic ESP32 LEDC register layout used by IDF's own LL HAL.
|
||||
// If another target without SOC_LEDC_SUPPORT_FADE_STOP is introduced, revisit this helper.
|
||||
static_assert(
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
true,
|
||||
#else
|
||||
false,
|
||||
#endif
|
||||
"LEDC duty_start pending check assumes classic ESP32 register layout; "
|
||||
"re-evaluate for this target");
|
||||
|
||||
static bool ledc_duty_update_pending(ledc_mode_t speed_mode, ledc_channel_t chan_num) {
|
||||
auto *hw = LEDC_LL_GET_HW();
|
||||
return hw->channel_group[speed_mode].channel[chan_num].conf1.duty_start != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) {
|
||||
return static_cast<float>(CLOCK_FREQUENCY) / static_cast<float>(1 << bit_depth);
|
||||
}
|
||||
@@ -105,21 +131,40 @@ void LEDCOutput::write_state(float state) {
|
||||
const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1;
|
||||
const float duty_rounded = roundf(state * max_duty);
|
||||
auto duty = static_cast<uint32_t>(duty_rounded);
|
||||
if (duty == this->last_duty_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_);
|
||||
auto speed_mode = get_speed_mode(this->channel_);
|
||||
auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8);
|
||||
int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_);
|
||||
if (duty == max_duty) {
|
||||
ledc_stop(speed_mode, chan_num, 1);
|
||||
this->last_duty_ = duty;
|
||||
} else if (duty == 0) {
|
||||
ledc_stop(speed_mode, chan_num, 0);
|
||||
this->last_duty_ = duty;
|
||||
} else {
|
||||
#if !defined(SOC_LEDC_SUPPORT_FADE_STOP)
|
||||
if (ledc_duty_update_pending(speed_mode, chan_num)) {
|
||||
ESP_LOGV(TAG, "Skipping LEDC duty update on channel %u while previous duty_start is still set", this->channel_);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint);
|
||||
ledc_update_duty(speed_mode, chan_num);
|
||||
this->last_duty_ = duty;
|
||||
}
|
||||
}
|
||||
|
||||
void LEDCOutput::setup() {
|
||||
if (!ledc_peripheral_reset_done) {
|
||||
ESP_LOGV(TAG, "Resetting LEDC peripheral to clear stale state after reboot");
|
||||
periph_module_reset(PERIPH_LEDC_MODULE);
|
||||
ledc_peripheral_reset_done = true;
|
||||
}
|
||||
|
||||
auto speed_mode = get_speed_mode(this->channel_);
|
||||
auto timer_num = static_cast<ledc_timer_t>((this->channel_ % 8) / 2);
|
||||
auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8);
|
||||
@@ -207,12 +252,12 @@ void LEDCOutput::update_frequency(float frequency) {
|
||||
this->status_clear_error();
|
||||
|
||||
// re-apply duty
|
||||
this->last_duty_ = UINT32_MAX;
|
||||
this->write_state(this->duty_);
|
||||
}
|
||||
|
||||
uint8_t next_ledc_channel = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace ledc
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ledc
|
||||
|
||||
#endif
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ledc {
|
||||
namespace esphome::ledc {
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern uint8_t next_ledc_channel;
|
||||
@@ -39,6 +39,7 @@ class LEDCOutput : public output::FloatOutput, public Component {
|
||||
float phase_angle_{0.0f};
|
||||
float frequency_{};
|
||||
float duty_{0.0f};
|
||||
uint32_t last_duty_{UINT32_MAX};
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
@@ -56,7 +57,6 @@ template<typename... Ts> class SetFrequencyAction : public Action<Ts...> {
|
||||
LEDCOutput *parent_;
|
||||
};
|
||||
|
||||
} // namespace ledc
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ledc
|
||||
|
||||
#endif
|
||||
|
||||
@@ -125,13 +125,17 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||
// Byte 0: modbus address (match all)
|
||||
if (at == 0)
|
||||
return true;
|
||||
uint8_t address = raw[0];
|
||||
uint8_t function_code = raw[1];
|
||||
// Byte 1: function code
|
||||
if (at == 1)
|
||||
return true;
|
||||
// Byte 2: Size (with modbus rtu function code 4/3)
|
||||
// See also https://en.wikipedia.org/wiki/Modbus
|
||||
if (at == 2)
|
||||
return true;
|
||||
|
||||
uint8_t address = raw[0];
|
||||
uint8_t function_code = raw[1];
|
||||
|
||||
uint8_t data_len = raw[2];
|
||||
uint8_t data_offset = 3;
|
||||
|
||||
@@ -146,10 +150,6 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||
// chance that this is a complete message ... admittedly there is a small chance is
|
||||
// isn't but that is quite small given the purpose of the CRC in the first place
|
||||
|
||||
// Fewer than 2 bytes can't calc CRC
|
||||
if (at < 2)
|
||||
return true;
|
||||
|
||||
data_len = at - 2;
|
||||
data_offset = 1;
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@ const std::string &OneWireDevice::get_address_name() {
|
||||
return this->address_name_;
|
||||
}
|
||||
|
||||
void OneWireDevice::set_address(uint64_t address) {
|
||||
this->address_ = address;
|
||||
this->address_name_.clear();
|
||||
}
|
||||
|
||||
bool OneWireDevice::send_command_(uint8_t cmd) {
|
||||
if (!this->bus_->select(this->address_))
|
||||
return false;
|
||||
|
||||
@@ -15,7 +15,7 @@ class OneWireDevice {
|
||||
public:
|
||||
/// @brief store the address of the device
|
||||
/// @param address of the device
|
||||
void set_address(uint64_t address) { this->address_ = address; }
|
||||
void set_address(uint64_t address);
|
||||
|
||||
void set_index(uint8_t index) { this->index_ = index; }
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class JPEGFormat(Format):
|
||||
|
||||
def actions(self) -> None:
|
||||
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
|
||||
cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2")
|
||||
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
|
||||
|
||||
|
||||
class PNGFormat(Format):
|
||||
|
||||
@@ -403,9 +403,9 @@ async def filter_out_filter_to_code(config, filter_id):
|
||||
QUANTILE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_QUANTILE, default=0.9): cv.zero_to_one_float,
|
||||
}
|
||||
),
|
||||
@@ -427,9 +427,9 @@ async def quantile_filter_to_code(config, filter_id):
|
||||
MEDIAN_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
@@ -449,9 +449,9 @@ async def median_filter_to_code(config, filter_id):
|
||||
MIN_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
@@ -483,9 +483,9 @@ async def min_filter_to_code(config, filter_id):
|
||||
MAX_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
@@ -509,9 +509,9 @@ async def max_filter_to_code(config, filter_id):
|
||||
SLIDING_AVERAGE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=15): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=15): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
@@ -540,8 +540,8 @@ EXPONENTIAL_AVERAGE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ALPHA, default=0.1): cv.positive_float,
|
||||
cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_EVERY, default=15): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
|
||||
@@ -41,26 +41,14 @@ void Filter::initialize(Sensor *parent, Filter *next) {
|
||||
}
|
||||
|
||||
// SlidingWindowFilter
|
||||
SlidingWindowFilter::SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at)
|
||||
: window_size_(window_size), send_every_(send_every), send_at_(send_every - send_first_at) {
|
||||
// Allocate ring buffer once at initialization
|
||||
SlidingWindowFilter::SlidingWindowFilter(uint16_t window_size, uint16_t send_every, uint16_t send_first_at)
|
||||
: send_every_(send_every), send_at_(send_every - send_first_at) {
|
||||
this->window_.init(window_size);
|
||||
}
|
||||
|
||||
optional<float> SlidingWindowFilter::new_value(float value) {
|
||||
// Add value to ring buffer
|
||||
if (this->window_count_ < this->window_size_) {
|
||||
// Buffer not yet full - just append
|
||||
this->window_.push_back(value);
|
||||
this->window_count_++;
|
||||
} else {
|
||||
// Buffer full - overwrite oldest value (ring buffer)
|
||||
this->window_[this->window_head_] = value;
|
||||
this->window_head_++;
|
||||
if (this->window_head_ >= this->window_size_) {
|
||||
this->window_head_ = 0;
|
||||
}
|
||||
}
|
||||
// Add value to ring buffer (overwrites oldest when full)
|
||||
this->window_.push_overwrite(value);
|
||||
|
||||
// Check if we should send a result
|
||||
if (++this->send_at_ >= this->send_every_) {
|
||||
@@ -77,9 +65,8 @@ FixedVector<float> SortedWindowFilter::get_window_values_() {
|
||||
// Copy window without NaN values using FixedVector (no heap allocation)
|
||||
// Returns unsorted values - caller will use std::nth_element for partial sorting as needed
|
||||
FixedVector<float> values;
|
||||
values.init(this->window_count_);
|
||||
for (size_t i = 0; i < this->window_count_; i++) {
|
||||
float v = this->window_[i];
|
||||
values.init(this->window_.size());
|
||||
for (float v : this->window_) {
|
||||
if (!std::isnan(v)) {
|
||||
values.push_back(v);
|
||||
}
|
||||
@@ -150,8 +137,7 @@ float MaxFilter::compute_result() { return this->find_extremum_<std::greater<flo
|
||||
float SlidingWindowMovingAverageFilter::compute_result() {
|
||||
float sum = 0;
|
||||
size_t valid_count = 0;
|
||||
for (size_t i = 0; i < this->window_count_; i++) {
|
||||
float v = this->window_[i];
|
||||
for (float v : this->window_) {
|
||||
if (!std::isnan(v)) {
|
||||
sum += v;
|
||||
valid_count++;
|
||||
@@ -161,7 +147,7 @@ float SlidingWindowMovingAverageFilter::compute_result() {
|
||||
}
|
||||
|
||||
// ExponentialMovingAverageFilter
|
||||
ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at)
|
||||
ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, uint16_t send_every, uint16_t send_first_at)
|
||||
: alpha_(alpha), send_every_(send_every), send_at_(send_every - send_first_at) {}
|
||||
optional<float> ExponentialMovingAverageFilter::new_value(float value) {
|
||||
if (!std::isnan(value)) {
|
||||
@@ -183,7 +169,7 @@ optional<float> ExponentialMovingAverageFilter::new_value(float value) {
|
||||
}
|
||||
return {};
|
||||
}
|
||||
void ExponentialMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
|
||||
void ExponentialMovingAverageFilter::set_send_every(uint16_t send_every) { this->send_every_ = send_every; }
|
||||
void ExponentialMovingAverageFilter::set_alpha(float alpha) { this->alpha_ = alpha; }
|
||||
|
||||
// ThrottleAverageFilter
|
||||
@@ -511,7 +497,7 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
|
||||
}
|
||||
|
||||
// StreamingFilter (base class)
|
||||
StreamingFilter::StreamingFilter(size_t window_size, size_t send_first_at)
|
||||
StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at)
|
||||
: window_size_(window_size), send_first_at_(send_first_at) {}
|
||||
|
||||
optional<float> StreamingFilter::new_value(float value) {
|
||||
|
||||
@@ -52,7 +52,7 @@ class Filter {
|
||||
*/
|
||||
class SlidingWindowFilter : public Filter {
|
||||
public:
|
||||
SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at);
|
||||
SlidingWindowFilter(uint16_t window_size, uint16_t send_every, uint16_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) final;
|
||||
|
||||
@@ -60,14 +60,10 @@ class SlidingWindowFilter : public Filter {
|
||||
/// Called by new_value() to compute the filtered result from the current window
|
||||
virtual float compute_result() = 0;
|
||||
|
||||
/// Access the sliding window values (ring buffer implementation)
|
||||
/// Use: for (size_t i = 0; i < window_count_; i++) { float val = window_[i]; }
|
||||
FixedVector<float> window_;
|
||||
size_t window_head_{0}; ///< Index where next value will be written
|
||||
size_t window_count_{0}; ///< Number of valid values in window (0 to window_size_)
|
||||
size_t window_size_; ///< Maximum window size
|
||||
size_t send_every_; ///< Send result every N values
|
||||
size_t send_at_; ///< Counter for send_every
|
||||
/// Sliding window ring buffer - automatically overwrites oldest values when full
|
||||
FixedRingBuffer<float> window_;
|
||||
uint16_t send_every_; ///< Send result every N values
|
||||
uint16_t send_at_; ///< Counter for send_every
|
||||
};
|
||||
|
||||
/** Base class for Min/Max filters.
|
||||
@@ -84,8 +80,7 @@ class MinMaxFilter : public SlidingWindowFilter {
|
||||
template<typename Compare> float find_extremum_() {
|
||||
float result = NAN;
|
||||
Compare comp;
|
||||
for (size_t i = 0; i < this->window_count_; i++) {
|
||||
float v = this->window_[i];
|
||||
for (float v : this->window_) {
|
||||
if (!std::isnan(v)) {
|
||||
result = std::isnan(result) ? v : (comp(v, result) ? v : result);
|
||||
}
|
||||
@@ -239,18 +234,18 @@ class SlidingWindowMovingAverageFilter : public SlidingWindowFilter {
|
||||
*/
|
||||
class ExponentialMovingAverageFilter : public Filter {
|
||||
public:
|
||||
ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at);
|
||||
ExponentialMovingAverageFilter(float alpha, uint16_t send_every, uint16_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
void set_send_every(size_t send_every);
|
||||
void set_send_every(uint16_t send_every);
|
||||
void set_alpha(float alpha);
|
||||
|
||||
protected:
|
||||
float accumulator_{NAN};
|
||||
float alpha_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
uint16_t send_every_;
|
||||
uint16_t send_at_;
|
||||
bool first_value_{true};
|
||||
};
|
||||
|
||||
@@ -570,7 +565,7 @@ class ToNTCTemperatureFilter : public Filter {
|
||||
*/
|
||||
class StreamingFilter : public Filter {
|
||||
public:
|
||||
StreamingFilter(size_t window_size, size_t send_first_at);
|
||||
StreamingFilter(uint16_t window_size, uint16_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) final;
|
||||
|
||||
@@ -584,9 +579,9 @@ class StreamingFilter : public Filter {
|
||||
/// Called by new_value() to reset internal state after sending a result
|
||||
virtual void reset_batch() = 0;
|
||||
|
||||
size_t window_size_;
|
||||
size_t count_{0};
|
||||
size_t send_first_at_;
|
||||
uint16_t window_size_;
|
||||
uint16_t count_{0};
|
||||
uint16_t send_first_at_;
|
||||
bool first_send_{true};
|
||||
};
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
#endif
|
||||
|
||||
if (ret != 0) {
|
||||
ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
|
||||
ESP_LOGW(TAG, "settimeofday() failed with code %d", ret);
|
||||
}
|
||||
#endif
|
||||
auto time = this->now();
|
||||
|
||||
@@ -105,15 +105,34 @@ void RP2040UartComponent::setup() {
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which hardware UART to use. A pin that is not specified
|
||||
// should not prevent hardware UART selection — one-way UART is valid.
|
||||
// When both pins are configured, both must be HW-capable and agree on UART number.
|
||||
// When only one pin is configured (nullptr other), use that pin's HW UART.
|
||||
// If a pin is configured but not HW-capable (inverted/invalid), fall back to SerialPIO.
|
||||
int8_t hw_uart = -1;
|
||||
const bool tx_configured = (this->tx_pin_ != nullptr);
|
||||
const bool rx_configured = (this->rx_pin_ != nullptr);
|
||||
|
||||
if (tx_configured && rx_configured) {
|
||||
// Both pins configured — both must map to the same hardware UART
|
||||
if (tx_hw != -1 && rx_hw != -1 && tx_hw == rx_hw) {
|
||||
hw_uart = tx_hw;
|
||||
}
|
||||
} else if (tx_configured) {
|
||||
hw_uart = tx_hw;
|
||||
} else if (rx_configured) {
|
||||
hw_uart = rx_hw;
|
||||
}
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
if (tx_hw == rx_hw && logger::global_logger->get_uart() == tx_hw) {
|
||||
ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", tx_hw);
|
||||
tx_hw = -1;
|
||||
rx_hw = -1;
|
||||
if (hw_uart != -1 && logger::global_logger->get_uart() == hw_uart) {
|
||||
ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", hw_uart);
|
||||
hw_uart = -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (tx_hw == -1 || rx_hw == -1 || tx_hw != rx_hw) {
|
||||
if (hw_uart == -1) {
|
||||
ESP_LOGV(TAG, "Using SerialPIO");
|
||||
pin_size_t tx = this->tx_pin_ == nullptr ? NOPIN : this->tx_pin_->get_pin();
|
||||
pin_size_t rx = this->rx_pin_ == nullptr ? NOPIN : this->rx_pin_->get_pin();
|
||||
@@ -127,13 +146,15 @@ void RP2040UartComponent::setup() {
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Using Hardware Serial");
|
||||
SerialUART *serial;
|
||||
if (tx_hw == 0) {
|
||||
if (hw_uart == 0) {
|
||||
serial = &Serial1;
|
||||
} else {
|
||||
serial = &Serial2;
|
||||
}
|
||||
serial->setTX(this->tx_pin_->get_pin());
|
||||
serial->setRX(this->rx_pin_->get_pin());
|
||||
if (this->tx_pin_ != nullptr)
|
||||
serial->setTX(this->tx_pin_->get_pin());
|
||||
if (this->rx_pin_ != nullptr)
|
||||
serial->setRX(this->rx_pin_->get_pin());
|
||||
serial->setFIFOSize(this->rx_buffer_size_);
|
||||
serial->begin(this->baud_rate_, config);
|
||||
this->serial_ = serial;
|
||||
|
||||
@@ -2197,7 +2197,7 @@ json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *we
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
|
||||
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_ALL);
|
||||
}
|
||||
json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
|
||||
+130
-1
@@ -304,7 +304,7 @@ template<typename T, size_t N> class StaticVector {
|
||||
/// Not thread-safe. All access (push/pop/iteration) must occur from a single
|
||||
/// context, or the caller must provide external synchronization.
|
||||
template<typename T, size_t N> class StaticRingBuffer {
|
||||
using index_type = std::conditional_t<(N <= 255), uint8_t, uint16_t>;
|
||||
using index_type = std::conditional_t<(N <= std::numeric_limits<uint8_t>::max()), uint8_t, uint16_t>;
|
||||
|
||||
public:
|
||||
class Iterator {
|
||||
@@ -359,6 +359,13 @@ template<typename T, size_t N> class StaticRingBuffer {
|
||||
index_type size() const { return this->count_; }
|
||||
bool empty() const { return this->count_ == 0; }
|
||||
|
||||
/// Clear all elements (reset to empty)
|
||||
void clear() {
|
||||
this->head_ = 0;
|
||||
this->tail_ = 0;
|
||||
this->count_ = 0;
|
||||
}
|
||||
|
||||
Iterator begin() { return Iterator(this, 0); }
|
||||
Iterator end() { return Iterator(this, this->count_); }
|
||||
ConstIterator begin() const { return ConstIterator(this, 0); }
|
||||
@@ -371,6 +378,128 @@ template<typename T, size_t N> class StaticRingBuffer {
|
||||
index_type count_{0};
|
||||
};
|
||||
|
||||
/// Fixed-capacity circular buffer - allocates once at runtime, never reallocates.
|
||||
/// Runtime-sized equivalent of StaticRingBuffer - use when capacity is only known at initialization.
|
||||
/// Supports FIFO push/pop and iteration over queued elements.
|
||||
/// Not thread-safe.
|
||||
template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::max()> class FixedRingBuffer {
|
||||
using index_type = std::conditional_t<
|
||||
(MAX_CAPACITY <= std::numeric_limits<uint8_t>::max()), uint8_t,
|
||||
std::conditional_t<(MAX_CAPACITY <= std::numeric_limits<uint16_t>::max()), uint16_t, uint32_t>>;
|
||||
|
||||
public:
|
||||
class Iterator {
|
||||
public:
|
||||
Iterator(FixedRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {}
|
||||
T &operator*() { return buf_->data_[(buf_->head_ + pos_) % buf_->capacity_]; }
|
||||
Iterator &operator++() {
|
||||
++pos_;
|
||||
return *this;
|
||||
}
|
||||
bool operator!=(const Iterator &other) const { return pos_ != other.pos_; }
|
||||
|
||||
private:
|
||||
FixedRingBuffer *buf_;
|
||||
index_type pos_;
|
||||
};
|
||||
|
||||
class ConstIterator {
|
||||
public:
|
||||
ConstIterator(const FixedRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {}
|
||||
const T &operator*() const { return buf_->data_[(buf_->head_ + pos_) % buf_->capacity_]; }
|
||||
ConstIterator &operator++() {
|
||||
++pos_;
|
||||
return *this;
|
||||
}
|
||||
bool operator!=(const ConstIterator &other) const { return pos_ != other.pos_; }
|
||||
|
||||
private:
|
||||
const FixedRingBuffer *buf_;
|
||||
index_type pos_;
|
||||
};
|
||||
|
||||
FixedRingBuffer() = default;
|
||||
~FixedRingBuffer() {
|
||||
if constexpr (std::is_trivial<T>::value) {
|
||||
::operator delete(this->data_);
|
||||
} else {
|
||||
delete[] this->data_;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable copy
|
||||
FixedRingBuffer(const FixedRingBuffer &) = delete;
|
||||
FixedRingBuffer &operator=(const FixedRingBuffer &) = delete;
|
||||
|
||||
/// Allocate capacity - can only be called once
|
||||
void init(index_type capacity) {
|
||||
if constexpr (std::is_trivial<T>::value) {
|
||||
// Raw allocation without initialization (elements are written before read)
|
||||
// NOLINTNEXTLINE(bugprone-sizeof-expression)
|
||||
this->data_ = static_cast<T *>(::operator new(capacity * sizeof(T)));
|
||||
} else {
|
||||
this->data_ = new T[capacity];
|
||||
}
|
||||
this->capacity_ = capacity;
|
||||
}
|
||||
|
||||
/// Push a value. Returns false if full.
|
||||
bool push(const T &value) {
|
||||
if (this->count_ >= this->capacity_)
|
||||
return false;
|
||||
this->data_[this->tail_] = value;
|
||||
this->tail_ = (this->tail_ + 1) % this->capacity_;
|
||||
++this->count_;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Push a value, overwriting the oldest if full.
|
||||
void push_overwrite(const T &value) {
|
||||
this->data_[this->tail_] = value;
|
||||
this->tail_ = (this->tail_ + 1) % this->capacity_;
|
||||
if (this->count_ >= this->capacity_) {
|
||||
// Buffer full - advance head to drop oldest, count stays at capacity
|
||||
this->head_ = this->tail_;
|
||||
} else {
|
||||
++this->count_;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the oldest element.
|
||||
void pop() {
|
||||
if (this->count_ > 0) {
|
||||
this->head_ = (this->head_ + 1) % this->capacity_;
|
||||
--this->count_;
|
||||
}
|
||||
}
|
||||
|
||||
T &front() { return this->data_[this->head_]; }
|
||||
const T &front() const { return this->data_[this->head_]; }
|
||||
index_type size() const { return this->count_; }
|
||||
bool empty() const { return this->count_ == 0; }
|
||||
index_type capacity() const { return this->capacity_; }
|
||||
bool full() const { return this->count_ == this->capacity_; }
|
||||
|
||||
/// Clear all elements (reset to empty, keep capacity)
|
||||
void clear() {
|
||||
this->head_ = 0;
|
||||
this->tail_ = 0;
|
||||
this->count_ = 0;
|
||||
}
|
||||
|
||||
Iterator begin() { return Iterator(this, 0); }
|
||||
Iterator end() { return Iterator(this, this->count_); }
|
||||
ConstIterator begin() const { return ConstIterator(this, 0); }
|
||||
ConstIterator end() const { return ConstIterator(this, this->count_); }
|
||||
|
||||
protected:
|
||||
T *data_{nullptr};
|
||||
index_type head_{0};
|
||||
index_type tail_{0};
|
||||
index_type count_{0};
|
||||
index_type capacity_{0};
|
||||
};
|
||||
|
||||
/// Fixed-capacity vector - allocates once at runtime, never reallocates
|
||||
/// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append)
|
||||
/// when size is known at initialization but not at compile time
|
||||
|
||||
@@ -4,7 +4,7 @@ dependencies:
|
||||
esphome/esp-audio-libs:
|
||||
version: 2.0.3
|
||||
esphome/micro-opus:
|
||||
version: 0.3.4
|
||||
version: 0.3.5
|
||||
espressif/esp-tflite-micro:
|
||||
version: 1.3.3~1
|
||||
espressif/esp32-camera:
|
||||
|
||||
+12
-7
@@ -2,6 +2,7 @@ import contextlib
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
import tempfile
|
||||
import time
|
||||
@@ -109,14 +110,18 @@ def prepare(
|
||||
CONF_CLIENT_CERTIFICATE_KEY
|
||||
):
|
||||
with (
|
||||
tempfile.NamedTemporaryFile(mode="w+") as cert_file,
|
||||
tempfile.NamedTemporaryFile(mode="w+") as key_file,
|
||||
tempfile.NamedTemporaryFile(mode="w+", delete=False) as cert_file,
|
||||
tempfile.NamedTemporaryFile(mode="w+", delete=False) as key_file,
|
||||
):
|
||||
cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE))
|
||||
cert_file.flush()
|
||||
key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY))
|
||||
key_file.flush()
|
||||
context.load_cert_chain(cert_file.name, key_file.name)
|
||||
try:
|
||||
cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE))
|
||||
key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY))
|
||||
cert_file.close()
|
||||
key_file.close()
|
||||
context.load_cert_chain(cert_file.name, key_file.name)
|
||||
finally:
|
||||
os.unlink(cert_file.name)
|
||||
os.unlink(key_file.name)
|
||||
client.tls_set_context(context)
|
||||
|
||||
try:
|
||||
|
||||
+2
-2
@@ -46,11 +46,11 @@ lib_deps_base =
|
||||
|
||||
lib_deps =
|
||||
${common.lib_deps_base}
|
||||
esphome/noise-c@0.1.11 ; api
|
||||
esphome/noise-c@0.1.11 ; api
|
||||
improv/Improv@1.2.4 ; improv_serial / esp32_improv
|
||||
kikuchan98/pngle@1.1.0 ; online_image
|
||||
; Using the repository directly, otherwise ESP-IDF can't use the library
|
||||
https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image
|
||||
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; online_image
|
||||
; This dependency is used only in unit tests.
|
||||
; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py
|
||||
; See scripts/cpp_unit_test.py and tests/components/README.md
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
sensor:
|
||||
- id: my_sensor
|
||||
platform: adc
|
||||
pin: VCC
|
||||
name: ADC Test sensor
|
||||
update_interval: "1:01"
|
||||
unit_of_measurement: "°C"
|
||||
icon: "mdi:water-percent"
|
||||
accuracy_decimals: 5
|
||||
setup_priority: -100
|
||||
force_update: true
|
||||
@@ -0,0 +1,59 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::modbus {
|
||||
|
||||
// Exposes protected methods for testing.
|
||||
class TestModbus : public Modbus {
|
||||
public:
|
||||
bool test_parse_modbus_byte(uint8_t byte) { return this->parse_modbus_byte_(byte); }
|
||||
void test_clear_rx_buffer() { this->rx_buffer_.clear(); }
|
||||
void set_waiting(uint8_t addr) { this->waiting_for_response_ = addr; }
|
||||
};
|
||||
|
||||
class MockDevice : public ModbusDevice {
|
||||
public:
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override { this->data_received = true; }
|
||||
bool data_received{false};
|
||||
};
|
||||
|
||||
TEST(ModbusTest, TwoByteRegressionTest) {
|
||||
TestModbus modbus;
|
||||
modbus.set_role(ModbusRole::CLIENT);
|
||||
// First byte (at=0)
|
||||
EXPECT_TRUE(modbus.test_parse_modbus_byte(0x01));
|
||||
// Second byte (at=1)
|
||||
// This used to reach raw[2] because it skipped the if(at==2) check, causing a
|
||||
// buffer overflow.
|
||||
EXPECT_TRUE(modbus.test_parse_modbus_byte(0x03));
|
||||
}
|
||||
|
||||
TEST(ModbusTest, TestValidFrame) {
|
||||
TestModbus modbus;
|
||||
modbus.set_role(ModbusRole::CLIENT);
|
||||
|
||||
MockDevice device;
|
||||
device.set_parent(&modbus);
|
||||
device.set_address(0x01);
|
||||
modbus.register_device(&device);
|
||||
modbus.set_waiting(0x01);
|
||||
|
||||
// Address 1, Function 3, Length 2, Data 0x1234
|
||||
uint8_t frame_data[] = {0x01, 0x03, 0x02, 0x12, 0x34};
|
||||
uint16_t crc = esphome::crc16(frame_data, sizeof(frame_data));
|
||||
|
||||
std::vector<uint8_t> frame;
|
||||
for (uint8_t b : frame_data)
|
||||
frame.push_back(b);
|
||||
frame.push_back(crc & 0xFF);
|
||||
frame.push_back((crc >> 8) & 0xFF);
|
||||
|
||||
for (size_t i = 0; i < frame.size(); i++) {
|
||||
bool result = modbus.test_parse_modbus_byte(frame[i]);
|
||||
EXPECT_TRUE(result) << "Failed at byte " << i << " (0x" << std::hex << (int) frame[i] << ")";
|
||||
}
|
||||
EXPECT_TRUE(device.data_received);
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus
|
||||
@@ -0,0 +1,6 @@
|
||||
substitutions:
|
||||
clk_pin: GPIO2
|
||||
mosi_pin: GPIO3
|
||||
miso_pin: GPIO4
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -23,3 +23,6 @@ uart:
|
||||
baud_rate: 115200
|
||||
debug:
|
||||
debug_prefix: "[UART1] "
|
||||
- id: uart_rx_only
|
||||
rx_pin: 17
|
||||
baud_rate: 1200
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: componenttestrp2040pico2ard
|
||||
friendly_name: $component_name
|
||||
|
||||
rp2040:
|
||||
board: rpipico2
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
packages:
|
||||
component_under_test: !include
|
||||
file: $component_test_file
|
||||
vars:
|
||||
component_test_file: $component_test_file
|
||||
@@ -0,0 +1,12 @@
|
||||
# Common SPI configuration for RP2040 Pico 2 (RP2350) Arduino tests
|
||||
|
||||
substitutions:
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO19
|
||||
miso_pin: GPIO16
|
||||
|
||||
spi:
|
||||
- id: spi_bus
|
||||
clk_pin: ${clk_pin}
|
||||
mosi_pin: ${mosi_pin}
|
||||
miso_pin: ${miso_pin}
|
||||
Reference in New Issue
Block a user