Merge branch 'refactor/sliding-window-static-ring-buffer' into integration

This commit is contained in:
J. Nick Koston
2026-03-12 11:58:34 -10:00
25 changed files with 398 additions and 99 deletions
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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")
+49 -4
View File
@@ -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 -4
View File
@@ -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
+6 -6
View File
@@ -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;
+5
View File
@@ -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;
+1 -1
View File
@@ -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; }
+1 -1
View File
@@ -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):
+17 -17
View File
@@ -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,
+10 -24
View File
@@ -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) {
+14 -19
View File
@@ -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};
};
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+59
View File
@@ -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}