From 1ab1768b6a21b0dd270706288a418f68df1fdb8c Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Mon, 10 Feb 2025 05:32:54 +0100 Subject: [PATCH] Add ADC sampling method option (#8131) Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> --- esphome/components/adc/__init__.py | 8 +++ esphome/components/adc/adc_sensor.h | 17 ++++++ esphome/components/adc/adc_sensor_common.cpp | 55 +++++++++++++++++++ esphome/components/adc/adc_sensor_esp32.cpp | 13 +++-- esphome/components/adc/adc_sensor_esp8266.cpp | 16 ++++-- .../components/adc/adc_sensor_libretiny.cpp | 17 ++++-- esphome/components/adc/adc_sensor_rp2040.cpp | 23 ++++---- esphome/components/adc/sensor.py | 12 +++- tests/component_tests/sensor/test_sensor.yaml | 1 + 9 files changed, 132 insertions(+), 30 deletions(-) diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index d8d21523b9..be420475fb 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -36,6 +36,14 @@ ATTENUATION_MODES = { "auto": "auto", } +sampling_mode = adc_ns.enum("SamplingMode", is_class=True) + +SAMPLING_MODES = { + "avg": sampling_mode.AVG, + "min": sampling_mode.MIN, + "max": sampling_mode.MAX, +} + adc1_channel_t = cg.global_ns.enum("adc1_channel_t") adc2_channel_t = cg.global_ns.enum("adc2_channel_t") diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 7a3e1c8da7..62f2461245 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -28,6 +28,21 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11; #endif #endif // USE_ESP32 +enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 }; +const LogString *sampling_mode_to_str(SamplingMode mode); + +class Aggregator { + public: + void add_sample(uint32_t value); + uint32_t aggregate(); + Aggregator(SamplingMode mode); + + protected: + SamplingMode mode_{SamplingMode::AVG}; + uint32_t aggr_{0}; + uint32_t samples_{0}; +}; + class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { public: #ifdef USE_ESP32 @@ -54,6 +69,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; } void set_sample_count(uint8_t sample_count); + void set_sampling_mode(SamplingMode sampling_mode); float sample() override; #ifdef USE_ESP8266 @@ -68,6 +84,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage InternalGPIOPin *pin_; bool output_raw_{false}; uint8_t sample_count_{1}; + SamplingMode sampling_mode_{SamplingMode::AVG}; #ifdef USE_RP2040 bool is_temperature_{false}; diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index 2dccd55fcd..c7509c7c7a 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -6,6 +6,59 @@ namespace adc { static const char *const TAG = "adc.common"; +const LogString *sampling_mode_to_str(SamplingMode mode) { + switch (mode) { + case SamplingMode::AVG: + return LOG_STR("average"); + case SamplingMode::MIN: + return LOG_STR("minimum"); + case SamplingMode::MAX: + return LOG_STR("maximum"); + } + return LOG_STR("unknown"); +} + +Aggregator::Aggregator(SamplingMode mode) { + this->mode_ = mode; + // set to max uint if mode is "min" + if (mode == SamplingMode::MIN) { + this->aggr_ = UINT32_MAX; + } +} + +void Aggregator::add_sample(uint32_t value) { + this->samples_ += 1; + + switch (this->mode_) { + case SamplingMode::AVG: + this->aggr_ += value; + break; + + case SamplingMode::MIN: + if (value < this->aggr_) { + this->aggr_ = value; + } + break; + + case SamplingMode::MAX: + if (value > this->aggr_) { + this->aggr_ = value; + } + } +} + +uint32_t Aggregator::aggregate() { + if (this->mode_ == SamplingMode::AVG) { + if (this->samples_ == 0) { + return this->aggr_; + } + + return (this->aggr_ + (this->samples_ >> 1)) / this->samples_; // NOLINT(clang-analyzer-core.DivideZero) + } + + return this->aggr_; +} + void ADCSensor::update() { float value_v = this->sample(); ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); @@ -18,6 +71,8 @@ void ADCSensor::set_sample_count(uint8_t sample_count) { } } +void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; } + float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } } // namespace adc diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index 24e3750091..0f1d802937 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -78,12 +78,14 @@ void ADCSensor::dump_config() { } } ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); + ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); LOG_UPDATE_INTERVAL(this); } float ADCSensor::sample() { if (!this->autorange_) { - uint32_t sum = 0; + auto aggr = Aggregator(this->sampling_mode_); + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { int raw = -1; if (this->channel1_ != ADC1_CHANNEL_MAX) { @@ -94,13 +96,14 @@ float ADCSensor::sample() { if (raw == -1) { return NAN; } - sum += raw; + + aggr.add_sample(raw); } - sum = (sum + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) if (this->output_raw_) { - return sum; + return aggr.aggregate(); } - uint32_t mv = esp_adc_cal_raw_to_voltage(sum, &this->cal_characteristics_[(int32_t) this->attenuation_]); + uint32_t mv = + esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]); return mv / 1000.0f; } diff --git a/esphome/components/adc/adc_sensor_esp8266.cpp b/esphome/components/adc/adc_sensor_esp8266.cpp index c9b6f8b652..9a12009abc 100644 --- a/esphome/components/adc/adc_sensor_esp8266.cpp +++ b/esphome/components/adc/adc_sensor_esp8266.cpp @@ -31,23 +31,27 @@ void ADCSensor::dump_config() { LOG_PIN(" Pin: ", this->pin_); #endif // USE_ADC_SENSOR_VCC ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); + ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); LOG_UPDATE_INTERVAL(this); } float ADCSensor::sample() { - uint32_t raw = 0; + auto aggr = Aggregator(this->sampling_mode_); + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + uint32_t raw = 0; #ifdef USE_ADC_SENSOR_VCC - raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) + raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) #else - raw += analogRead(this->pin_->get_pin()); // NOLINT + raw = analogRead(this->pin_->get_pin()); // NOLINT #endif // USE_ADC_SENSOR_VCC + aggr.add_sample(raw); } - raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + if (this->output_raw_) { - return raw; + return aggr.aggregate(); } - return raw / 1024.0f; + return aggr.aggregate() / 1024.0f; } std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } diff --git a/esphome/components/adc/adc_sensor_libretiny.cpp b/esphome/components/adc/adc_sensor_libretiny.cpp index cd04477b3f..9e75ed414c 100644 --- a/esphome/components/adc/adc_sensor_libretiny.cpp +++ b/esphome/components/adc/adc_sensor_libretiny.cpp @@ -23,23 +23,28 @@ void ADCSensor::dump_config() { LOG_PIN(" Pin: ", this->pin_); #endif // USE_ADC_SENSOR_VCC ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); + ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); LOG_UPDATE_INTERVAL(this); } float ADCSensor::sample() { uint32_t raw = 0; + auto aggr = Aggregator(this->sampling_mode_); + if (this->output_raw_) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) { - raw += analogRead(this->pin_->get_pin()); // NOLINT + raw = analogRead(this->pin_->get_pin()); // NOLINT + aggr.add_sample(raw); } - raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) - return raw; + return aggr.aggregate(); } + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { - raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT + raw = analogReadVoltage(this->pin_->get_pin()); // NOLINT + aggr.add_sample(raw); } - raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) - return raw / 1000.0f; + + return aggr.aggregate() / 1000.0f; } } // namespace adc diff --git a/esphome/components/adc/adc_sensor_rp2040.cpp b/esphome/components/adc/adc_sensor_rp2040.cpp index 520ff3bacc..f6cf1bac7a 100644 --- a/esphome/components/adc/adc_sensor_rp2040.cpp +++ b/esphome/components/adc/adc_sensor_rp2040.cpp @@ -34,24 +34,28 @@ void ADCSensor::dump_config() { #endif // USE_ADC_SENSOR_VCC } ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); + ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); LOG_UPDATE_INTERVAL(this); } float ADCSensor::sample() { + uint32_t raw = 0; + auto aggr = Aggregator(this->sampling_mode_); + if (this->is_temperature_) { adc_set_temp_sensor_enabled(true); delay(1); adc_select_input(4); - uint32_t raw = 0; + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { - raw += adc_read(); + raw = adc_read(); + aggr.add_sample(raw); } - raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) adc_set_temp_sensor_enabled(false); if (this->output_raw_) { - return raw; + return aggr.aggregate(); } - return raw * 3.3f / 4096.0f; + return aggr.aggregate() * 3.3f / 4096.0f; } uint8_t pin = this->pin_->get_pin(); @@ -68,11 +72,10 @@ float ADCSensor::sample() { adc_gpio_init(pin); adc_select_input(pin - 26); - uint32_t raw = 0; for (uint8_t sample = 0; sample < this->sample_count_; sample++) { - raw += adc_read(); + raw = adc_read(); + aggr.add_sample(raw); } - raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) #ifdef CYW43_USES_VSYS_PIN if (pin == PICO_VSYS_PIN) { @@ -81,10 +84,10 @@ float ADCSensor::sample() { #endif // CYW43_USES_VSYS_PIN if (this->output_raw_) { - return raw; + return aggr.aggregate(); } float coeff = pin == PICO_VSYS_PIN ? 3.0f : 1.0f; - return raw * 3.3f / 4096.0f * coeff; + return aggr.aggregate() * 3.3f / 4096.0f * coeff; } } // namespace adc diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 59ea9e184c..3309bd04c5 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -1,11 +1,9 @@ import logging import esphome.codegen as cg -import esphome.config_validation as cv -import esphome.final_validate as fv -from esphome.core import CORE from esphome.components import sensor, voltage_sampler from esphome.components.esp32 import get_esp32_variant +import esphome.config_validation as cv from esphome.const import ( CONF_ATTENUATION, CONF_ID, @@ -17,10 +15,14 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) +from esphome.core import CORE +import esphome.final_validate as fv + from . import ( ATTENUATION_MODES, ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, + SAMPLING_MODES, adc_ns, validate_adc_pin, ) @@ -30,9 +32,11 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["voltage_sampler"] CONF_SAMPLES = "samples" +CONF_SAMPLING_MODE = "sampling_mode" _attenuation = cv.enum(ATTENUATION_MODES, lower=True) +_sampling_mode = cv.enum(SAMPLING_MODES, lower=True) def validate_config(config): @@ -88,6 +92,7 @@ CONFIG_SCHEMA = cv.All( cv.only_on_esp32, _attenuation ), cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255), + cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode, } ) .extend(cv.polling_component_schema("60s")), @@ -112,6 +117,7 @@ async def to_code(config): cg.add(var.set_output_raw(config[CONF_RAW])) cg.add(var.set_sample_count(config[CONF_SAMPLES])) + cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) if attenuation := config.get(CONF_ATTENUATION): if attenuation == "auto": diff --git a/tests/component_tests/sensor/test_sensor.yaml b/tests/component_tests/sensor/test_sensor.yaml index 612b8e5e56..0f0ad5e94e 100644 --- a/tests/component_tests/sensor/test_sensor.yaml +++ b/tests/component_tests/sensor/test_sensor.yaml @@ -10,5 +10,6 @@ sensor: pin: A0 id: s_1 name: test s1 + sampling_mode: min update_interval: 60s device_class: voltage